Initial commit with version 0.1.2
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
commit
61dda71a56
|
@ -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,60 @@
|
||||||
|
# build:0
|
||||||
|
labels:
|
||||||
|
location: manager
|
||||||
|
clone:
|
||||||
|
git:
|
||||||
|
image: woodpeckerci/plugin-git
|
||||||
|
settings:
|
||||||
|
partial: false
|
||||||
|
depth: 1
|
||||||
|
when:
|
||||||
|
branch: [main]
|
||||||
|
steps:
|
||||||
|
# Build and Push
|
||||||
|
build-push:
|
||||||
|
name: build-push
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
environment:
|
||||||
|
REGISTRY_USER:
|
||||||
|
from_secret: REGISTRY_USER
|
||||||
|
REGISTRY_PASSWORD:
|
||||||
|
from_secret: REGISTRY_PASSWORD
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- echo "nameserver 1.1.1.1" > /etc/resolv.conf
|
||||||
|
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||||
|
- HOSTNAME=$(docker info --format "{{.Name}}")
|
||||||
|
- echo "Building on $HOSTNAME"
|
||||||
|
- echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us
|
||||||
|
- export PLOUGHSHARES_VERSION=$(cat VERSION | tr -d '\n')
|
||||||
|
- echo "Building version $${PLOUGHSHARES_VERSION}"
|
||||||
|
- docker build -t git.nixc.us/colin/ploughshares:latest -t git.nixc.us/colin/ploughshares:$${PLOUGHSHARES_VERSION} --build-arg APP_VERSION=$${PLOUGHSHARES_VERSION} -f docker/ploughshares/Dockerfile ./docker/ploughshares
|
||||||
|
- docker push git.nixc.us/colin/ploughshares:latest
|
||||||
|
- docker push git.nixc.us/colin/ploughshares:$${PLOUGHSHARES_VERSION}
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: [push]
|
||||||
|
|
||||||
|
# Deploy Production
|
||||||
|
deploy:
|
||||||
|
name: deploy
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
environment:
|
||||||
|
REGISTRY_USER:
|
||||||
|
from_secret: REGISTRY_USER
|
||||||
|
REGISTRY_PASSWORD:
|
||||||
|
from_secret: REGISTRY_PASSWORD
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- echo "nameserver 1.1.1.1" > /etc/resolv.conf
|
||||||
|
- echo "nameserver 1.0.0.1" >> /etc/resolv.conf
|
||||||
|
- HOSTNAME=$(docker info --format "{{.Name}}")
|
||||||
|
- echo "Deploying on $HOSTNAME"
|
||||||
|
- echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us
|
||||||
|
# No need to create secrets, using direct environment variables
|
||||||
|
- docker stack deploy --with-registry-auth -c stack.production.yml ploughshares
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: [push]
|
|
@ -0,0 +1,244 @@
|
||||||
|
# Project Ploughshares
|
||||||
|
|
||||||
|
[](https://woodpecker.nixc.us/colin/ploughshares)
|
||||||
|
|
||||||
|
A transaction management system.
|
||||||
|
|
||||||
|
Last updated: Thu Jul 3 13:16:40 EDT 2025
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
For local development, use the development Docker Compose configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Mount the source code as a volume for live reloading
|
||||||
|
- Enable Flask debug mode
|
||||||
|
- Expose the PostgreSQL port for direct access
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
For production deployment, use the production stack configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stack deploy -c stack.production.yml ploughshares
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to create the required Docker secrets first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "your-secure-password" | docker secret create db_password -
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
For staging deployment, use the staging stack configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stack deploy -c stack.staging.yml ploughshares-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure to create the required Docker secrets first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "your-staging-password" | docker secret create db_password_staging -
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
This project uses Woodpecker CI for continuous integration and deployment. The pipeline:
|
||||||
|
|
||||||
|
1. Builds the Docker image for multiple architectures
|
||||||
|
2. Pushes the image to the registry
|
||||||
|
3. Deploys to the production environment
|
||||||
|
4. Sends a notification about the deployment status
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
- `docker-compose.yml` - Default configuration for quick setup
|
||||||
|
- `docker-compose.dev.yml` - Development configuration with live reloading
|
||||||
|
- `stack.production.yml` - Production deployment with Docker Swarm
|
||||||
|
- `stack.staging.yml` - Staging deployment with Docker Swarm
|
||||||
|
- `.woodpecker.yml` - CI/CD pipeline configuration
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
The application uses PostgreSQL for data storage. The database schema is automatically initialized using the `schema.sql` file.
|
||||||
|
|
||||||
|
To generate test data, use the script in the tests directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inside the Docker container
|
||||||
|
python tests/generate_test_data.py --count 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The application provides a RESTful API for managing transactions. See the API documentation at `/api-docs` when the application is running.
|
||||||
|
|
||||||
|
## Version Management
|
||||||
|
|
||||||
|
The application uses semantic versioning (X.Y.Z) with the following components:
|
||||||
|
|
||||||
|
- The `VERSION` file at the root of the repository is the single source of truth for the application version
|
||||||
|
- The web UI and application automatically read the version from this file
|
||||||
|
- Version changes are managed using the `versionbump.sh` script
|
||||||
|
- A version history log is maintained in `version_history.log`
|
||||||
|
|
||||||
|
### Version Bump Script
|
||||||
|
|
||||||
|
The `versionbump.sh` script provides the following commands:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
# To set a specific version
|
||||||
|
./versionbump.sh set X.Y.Z
|
||||||
|
|
||||||
|
# To show help information
|
||||||
|
./versionbump.sh --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Consistency
|
||||||
|
|
||||||
|
The version is maintained in:
|
||||||
|
- `VERSION` file (source of truth)
|
||||||
|
- Docker Compose environment variables (APP_VERSION)
|
||||||
|
|
||||||
|
The application reads the version from:
|
||||||
|
1. The APP_VERSION environment variable if set
|
||||||
|
2. The VERSION file in the current directory
|
||||||
|
3. The VERSION file at the root of the repository
|
||||||
|
|
||||||
|
## Code Quality and Security
|
||||||
|
|
||||||
|
**IMPORTANT**: Code quality and security tools are **REQUIRED** for this project.
|
||||||
|
|
||||||
|
Install the necessary tools with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install-codechecks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs:
|
||||||
|
- flake8: Code style checker
|
||||||
|
- safety: Dependency vulnerability scanner
|
||||||
|
- bandit: Security issue scanner
|
||||||
|
- pytest: Testing framework
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
The project includes tests for:
|
||||||
|
- Core application functionality
|
||||||
|
- Code quality standards
|
||||||
|
- Dependency vulnerability checking
|
||||||
|
|
||||||
|
Run tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-commit Hooks
|
||||||
|
|
||||||
|
Git pre-commit hooks automatically run tests before allowing commits, ensuring code quality is maintained.
|
||||||
|
|
||||||
|
## 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,49 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ./docker/ploughshares
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ploughshares:dev
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
environment:
|
||||||
|
- FLASK_RUN_PORT=5001
|
||||||
|
- FLASK_ENV=development
|
||||||
|
- FLASK_DEBUG=1
|
||||||
|
- POSTGRES_HOST=db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD=ploughshares_password
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./docker/ploughshares:/app
|
||||||
|
- ./docker/ploughshares/uploads:/app/uploads
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:12
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD=ploughshares_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_dev_data:/var/lib/postgresql/data
|
||||||
|
- ./docker/ploughshares/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ploughshares"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_dev_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: ploughshares-dev
|
|
@ -0,0 +1,47 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ./docker/ploughshares
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
image: ploughshares:latest
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
environment:
|
||||||
|
- FLASK_RUN_PORT=5001
|
||||||
|
- POSTGRES_HOST=db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD=ploughshares_password
|
||||||
|
- APP_VERSION=0.1.2
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./docker/ploughshares/uploads:/app/uploads
|
||||||
|
|
||||||
|
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
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ploughshares"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ploughshares-network:
|
||||||
|
driver: bridge
|
|
@ -0,0 +1,32 @@
|
||||||
|
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 schema.sql .
|
||||||
|
COPY templates/ ./templates/
|
||||||
|
COPY static/ ./static/
|
||||||
|
# Tests directory is empty or doesn't contain required files
|
||||||
|
# COPY tests/ ./tests/
|
||||||
|
|
||||||
|
# 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,345 @@
|
||||||
|
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 locale
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('ploughshares')
|
||||||
|
|
||||||
|
# Get version from environment variable or VERSION file
|
||||||
|
VERSION = os.environ.get('APP_VERSION')
|
||||||
|
if not VERSION:
|
||||||
|
try:
|
||||||
|
# Try to read from VERSION file in the current directory first
|
||||||
|
if os.path.exists('VERSION'):
|
||||||
|
with open('VERSION', 'r') as f:
|
||||||
|
VERSION = f.read().strip()
|
||||||
|
# Fall back to the project root VERSION file
|
||||||
|
else:
|
||||||
|
with open(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'VERSION'), 'r') as f:
|
||||||
|
VERSION = f.read().strip()
|
||||||
|
except:
|
||||||
|
VERSION = "unknown"
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# Convert to float first, then format to exactly 2 decimal places
|
||||||
|
float_value = float(value)
|
||||||
|
return f"${float_value:,.2f}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Fallback formatting
|
||||||
|
return "$0.00"
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
logger.info(f"Ploughshares v{VERSION} - Connected to PostgreSQL at {host}:{port}")
|
||||||
|
return conn
|
||||||
|
except psycopg2.OperationalError as e:
|
||||||
|
logger.error(f"Error connecting to PostgreSQL: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- Routes ---
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
conn = get_db_connection()
|
||||||
|
if conn is None:
|
||||||
|
flash("Database connection error", "error")
|
||||||
|
return render_template('index.html', transactions=[], version=VERSION)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT * FROM transactions ORDER BY id DESC')
|
||||||
|
transactions = cur.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database error: {e}")
|
||||||
|
flash(f"Database error: {e}", "error")
|
||||||
|
transactions = []
|
||||||
|
finally:
|
||||||
|
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()
|
||||||
|
if conn is None:
|
||||||
|
flash("Database connection error", "error")
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
transaction = None
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||||
|
transaction = cur.fetchone()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database error: {e}")
|
||||||
|
flash(f"Database error: {e}", "error")
|
||||||
|
finally:
|
||||||
|
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
|
||||||
|
|
||||||
|
# Convert amount to float for database storage
|
||||||
|
if 'amount' in data and data['amount']:
|
||||||
|
try:
|
||||||
|
# Remove currency symbols and commas
|
||||||
|
clean_amount = data['amount'].replace('$', '').replace(',', '')
|
||||||
|
data['amount'] = float(clean_amount)
|
||||||
|
except ValueError:
|
||||||
|
data['amount'] = 0.0
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if conn is None:
|
||||||
|
flash("Database connection error", "error")
|
||||||
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO transactions (
|
||||||
|
transaction_type, company_division, address_1, address_2,
|
||||||
|
city, province, region, postal_code, is_primary, source_date, source_description,
|
||||||
|
grant_type, description, amount, recipient, commodity_class, contract_number, comments
|
||||||
|
) VALUES (
|
||||||
|
%(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s,
|
||||||
|
%(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s,
|
||||||
|
%(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s,
|
||||||
|
%(commodity_class)s, %(contract_number)s, %(comments)s
|
||||||
|
) RETURNING id
|
||||||
|
""",
|
||||||
|
data
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result and 'id' in result:
|
||||||
|
new_transaction_id = result['id']
|
||||||
|
conn.commit()
|
||||||
|
else:
|
||||||
|
flash("Error creating transaction: Could not get transaction ID", "error")
|
||||||
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating transaction: {e}")
|
||||||
|
flash(f"Error creating transaction: {e}", "error")
|
||||||
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
|
finally:
|
||||||
|
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 conn is None:
|
||||||
|
flash("Database connection error", "error")
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Convert amount to float for database storage
|
||||||
|
if 'amount' in data and data['amount']:
|
||||||
|
try:
|
||||||
|
# Remove currency symbols and commas
|
||||||
|
clean_amount = data['amount'].replace('$', '').replace(',', '')
|
||||||
|
data['amount'] = float(clean_amount)
|
||||||
|
except ValueError:
|
||||||
|
data['amount'] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
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()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating transaction: {e}")
|
||||||
|
flash(f"Error updating transaction: {e}", "error")
|
||||||
|
return render_template('transaction_form.html', transaction=data, version=VERSION)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return redirect(url_for('view_transaction', id=id))
|
||||||
|
|
||||||
|
transaction = None
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||||
|
transaction = cur.fetchone()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving transaction: {e}")
|
||||||
|
flash(f"Error retrieving transaction: {e}", "error")
|
||||||
|
finally:
|
||||||
|
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 initializes the schema if needed.
|
||||||
|
Test data can be loaded separately using the tests/generate_test_data.py script.
|
||||||
|
"""
|
||||||
|
logger.info(f"Ploughshares v{VERSION} - Checking database...")
|
||||||
|
conn = get_db_connection()
|
||||||
|
if conn is None:
|
||||||
|
logger.error("Database connection failed. Exiting.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if the transactions table exists
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'transactions')")
|
||||||
|
result = cur.fetchone()
|
||||||
|
|
||||||
|
# If table doesn't exist, initialize the schema
|
||||||
|
if not result or not result.get('exists', False):
|
||||||
|
logger.info(f"Ploughshares v{VERSION} - Transactions table not found. Initializing schema...")
|
||||||
|
|
||||||
|
# Read schema.sql file
|
||||||
|
try:
|
||||||
|
with open('schema.sql', 'r') as f:
|
||||||
|
schema_sql = f.read()
|
||||||
|
|
||||||
|
# Execute schema SQL
|
||||||
|
with conn.cursor() as schema_cur:
|
||||||
|
schema_cur.execute(schema_sql)
|
||||||
|
logger.info(f"Ploughshares v{VERSION} - Schema initialized successfully.")
|
||||||
|
except Exception as schema_error:
|
||||||
|
logger.error(f"Error initializing schema: {schema_error}")
|
||||||
|
else:
|
||||||
|
# Check if the table is empty
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT COUNT(*) FROM transactions")
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result and 'count' in result:
|
||||||
|
count = result['count']
|
||||||
|
if count == 0:
|
||||||
|
logger.info(f"Ploughshares v{VERSION} - Database is empty. You can populate it with test data using the tests/generate_test_data.py script.")
|
||||||
|
else:
|
||||||
|
logger.error("Could not get count from database")
|
||||||
|
except Exception as count_error:
|
||||||
|
logger.error(f"Error counting transactions: {count_error}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking database: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger.info(f"Starting Ploughshares v{VERSION}")
|
||||||
|
bootstrap_database()
|
||||||
|
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,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==3.1.1
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
requests==2.32.2
|
||||||
|
Faker==15.3.3
|
||||||
|
gunicorn==23.0.0
|
||||||
|
Werkzeug==3.1.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==2.1.3
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
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 IF NOT EXISTS transactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
transaction_type VARCHAR(255),
|
||||||
|
company_division VARCHAR(255),
|
||||||
|
address_1 VARCHAR(255),
|
||||||
|
address_2 VARCHAR(255),
|
||||||
|
city VARCHAR(255),
|
||||||
|
province VARCHAR(255),
|
||||||
|
region VARCHAR(255),
|
||||||
|
postal_code VARCHAR(50),
|
||||||
|
is_primary BOOLEAN DEFAULT FALSE,
|
||||||
|
source_date DATE,
|
||||||
|
source_description TEXT,
|
||||||
|
grant_type VARCHAR(255),
|
||||||
|
description TEXT,
|
||||||
|
amount NUMERIC(15, 2),
|
||||||
|
recipient VARCHAR(255),
|
||||||
|
commodity_class VARCHAR(255),
|
||||||
|
contract_number VARCHAR(255),
|
||||||
|
comments TEXT,
|
||||||
|
created_at TIMESTAMP 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 IF NOT EXISTS idx_transactions_type ON transactions(transaction_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transactions_division ON transactions(company_division);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transactions_recipient ON transactions(recipient);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transaction_date ON transactions(source_date);
|
|
@ -0,0 +1,7 @@
|
||||||
|
<html>
|
||||||
|
<head><title>404 Not Found</title></head>
|
||||||
|
<body>
|
||||||
|
<center><h1>404 Not Found</h1></center>
|
||||||
|
<hr><center>nginx</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,182 @@
|
||||||
|
{% 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>
|
||||||
|
|
||||||
|
<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,116 @@
|
||||||
|
<!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 rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
.navbar-nav {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
</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 ms-auto">
|
||||||
|
<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['id'] }}</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['id'] }} - 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['id'] }} - 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,80 @@
|
||||||
|
# Ploughshares Application Test and Deployment Report
|
||||||
|
|
||||||
|
## Deployment Status
|
||||||
|
|
||||||
|
✅ **Application successfully deployed**
|
||||||
|
- Web application running at http://localhost:5001
|
||||||
|
- API documentation available at http://localhost:5001/api-docs
|
||||||
|
- Database connected and healthy
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
### Functionality Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- Core imports verified
|
||||||
|
- API routes verified
|
||||||
|
|
||||||
|
### Code Quality Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- Python syntax valid
|
||||||
|
- No inappropriate print statements (in files other than app.py)
|
||||||
|
|
||||||
|
### Dependency Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- requirements.txt exists and is valid
|
||||||
|
- No known vulnerable package versions
|
||||||
|
|
||||||
|
## Security Scan Results
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Safety)
|
||||||
|
|
||||||
|
✅ **No vulnerabilities detected in dependencies**
|
||||||
|
|
||||||
|
All dependencies have been updated to secure versions:
|
||||||
|
- Flask: 2.2.2 → 3.1.1
|
||||||
|
- psycopg2-binary: 2.9.3 → 2.9.9
|
||||||
|
- requests: 2.28.1 → 2.32.2
|
||||||
|
- gunicorn: 20.1.0 → 23.0.0
|
||||||
|
- Werkzeug: 2.3.7 → 3.1.0 (updated from 3.0.6 to resolve dependency conflict)
|
||||||
|
- Jinja2: 3.1.2 → 3.1.6
|
||||||
|
- itsdangerous: 2.1.2 → 2.2.0 (updated to resolve dependency conflict)
|
||||||
|
|
||||||
|
### Code Security Issues (Bandit)
|
||||||
|
|
||||||
|
⚠️ **5 potential security issues detected**
|
||||||
|
|
||||||
|
1. **Hardcoded Password String** (Severity: Low, Confidence: Medium)
|
||||||
|
- Location: app.py:20
|
||||||
|
- Issue: `app.secret_key = 'supersecretkey'`
|
||||||
|
- CWE-259: Use of Hard-coded Password
|
||||||
|
|
||||||
|
2. **Binding to All Interfaces** (Severity: Medium, Confidence: Medium)
|
||||||
|
- Location: app.py:220
|
||||||
|
- Issue: `app.run(host='0.0.0.0', port=port)`
|
||||||
|
- CWE-605: Multiple Binds to the Same Port
|
||||||
|
|
||||||
|
3. **Hardcoded Password in Function Argument** (Severity: Low, Confidence: Medium)
|
||||||
|
- Location: init_db.py:6-11
|
||||||
|
- Issue: `password="testpass"` in database connection
|
||||||
|
- CWE-259: Use of Hard-coded Password
|
||||||
|
|
||||||
|
4-5. **Duplicate issues in app_fixed.py** (backup file)
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Immediate Actions
|
||||||
|
1. ✅ **Update vulnerable dependencies** - COMPLETED
|
||||||
|
2. **Replace hardcoded secrets** with environment variables
|
||||||
|
3. **Restrict network binding** in production environments
|
||||||
|
|
||||||
|
### Long-term Improvements
|
||||||
|
1. Implement **secret management** solution
|
||||||
|
2. Add **continuous security scanning** in CI/CD pipeline
|
||||||
|
3. Establish **dependency update policy**
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Run `./install-codechecks.sh` to install all required code quality tools
|
||||||
|
2. Address remaining security findings by:
|
||||||
|
- Moving secrets to environment variables
|
||||||
|
- Limiting network binding in production
|
||||||
|
- Removing hardcoded passwords in test scripts
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# install-codechecks.sh
|
||||||
|
# Installs required tools for code quality checks and security scanning
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e # Exit immediately if a command exits with non-zero status
|
||||||
|
|
||||||
|
echo "=== Installing Code Quality and Security Tools ==="
|
||||||
|
echo "These tools are REQUIRED for maintaining code quality and security standards."
|
||||||
|
|
||||||
|
# Check if pip is installed
|
||||||
|
if ! command -v pip3 &> /dev/null; then
|
||||||
|
echo "Error: pip3 is not installed. Please install Python and pip first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing flake8 (code style checker)..."
|
||||||
|
pip3 install flake8
|
||||||
|
|
||||||
|
echo "Installing safety (dependency vulnerability scanner)..."
|
||||||
|
pip3 install safety
|
||||||
|
|
||||||
|
echo "Installing bandit (security issue scanner)..."
|
||||||
|
pip3 install bandit
|
||||||
|
|
||||||
|
echo "Installing pytest (testing framework)..."
|
||||||
|
pip3 install pytest
|
||||||
|
|
||||||
|
# Create a local configuration for flake8
|
||||||
|
echo "Creating flake8 configuration..."
|
||||||
|
cat > .flake8 << EOF
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 100
|
||||||
|
exclude = .git,__pycache__,build,dist
|
||||||
|
ignore = E203,W503
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create a local configuration for bandit
|
||||||
|
echo "Creating bandit configuration..."
|
||||||
|
cat > .banditrc << EOF
|
||||||
|
[bandit]
|
||||||
|
exclude: /tests,/venv,/.venv
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Setting up Git hooks..."
|
||||||
|
# Ensure the Git hooks directory exists
|
||||||
|
mkdir -p .git/hooks
|
||||||
|
|
||||||
|
# Make sure the pre-commit hook is executable
|
||||||
|
if [ -f .git/hooks/pre-commit ]; then
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Installation Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT: These tools are required for maintaining code quality and security."
|
||||||
|
echo "Run the following commands to check your code:"
|
||||||
|
echo " - flake8 . # Check code style"
|
||||||
|
echo " - safety check -r docker/ploughshares/requirements.txt # Check for vulnerable dependencies"
|
||||||
|
echo " - bandit -r docker/ploughshares/ # Check for security issues"
|
||||||
|
echo " - python3 -m pytest tests/ # Run all tests"
|
||||||
|
echo ""
|
||||||
|
echo "Pre-commit hooks are installed to run tests automatically before commits."
|
||||||
|
echo ""
|
||||||
|
echo "Happy coding!"
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Ploughshares Security and Code Quality Scan Results (Updated)
|
||||||
|
|
||||||
|
## Deployment Status
|
||||||
|
|
||||||
|
✅ **Application successfully deployed**
|
||||||
|
- Web application running at http://localhost:5001
|
||||||
|
- API documentation available at http://localhost:5001/api-docs
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
### Functionality Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- Core imports verified
|
||||||
|
- API routes verified
|
||||||
|
|
||||||
|
### Code Quality Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- Python syntax valid
|
||||||
|
- No inappropriate print statements
|
||||||
|
|
||||||
|
### Dependency Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- requirements.txt exists
|
||||||
|
- No known vulnerable package versions
|
||||||
|
|
||||||
|
## Security Scan Results
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Safety)
|
||||||
|
|
||||||
|
✅ **No vulnerabilities detected in dependencies**
|
||||||
|
|
||||||
|
All dependencies have been updated to secure versions:
|
||||||
|
- Flask: 2.2.2 → 3.1.1
|
||||||
|
- psycopg2-binary: 2.9.3 → 2.9.9
|
||||||
|
- requests: 2.28.1 → 2.32.2
|
||||||
|
- gunicorn: 20.1.0 → 23.0.0
|
||||||
|
- Werkzeug: 2.3.7 → 3.0.6
|
||||||
|
- Jinja2: 3.1.2 → 3.1.6
|
||||||
|
|
||||||
|
### Code Security Issues (Bandit)
|
||||||
|
|
||||||
|
⚠️ **3 potential security issues remain to be addressed**
|
||||||
|
|
||||||
|
1. **Hardcoded Password String** (Severity: Low, Confidence: Medium)
|
||||||
|
- Location: app.py:20
|
||||||
|
- Issue: `app.secret_key = 'supersecretkey'`
|
||||||
|
- CWE-259: Use of Hard-coded Password
|
||||||
|
|
||||||
|
2. **Binding to All Interfaces** (Severity: Medium, Confidence: Medium)
|
||||||
|
- Location: app.py:220
|
||||||
|
- Issue: `app.run(host='0.0.0.0', port=port)`
|
||||||
|
- CWE-605: Multiple Binds to the Same Port
|
||||||
|
|
||||||
|
3. **Hardcoded Password in Function Argument** (Severity: Low, Confidence: Medium)
|
||||||
|
- Location: init_db.py:6-11
|
||||||
|
- Issue: `password="testpass"` in database connection
|
||||||
|
- CWE-259: Use of Hard-coded Password
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Immediate Actions
|
||||||
|
1. ✅ **Update vulnerable dependencies** - COMPLETED
|
||||||
|
2. **Replace hardcoded secrets** with environment variables
|
||||||
|
3. **Restrict network binding** in development environments
|
||||||
|
|
||||||
|
### Long-term Improvements
|
||||||
|
1. Implement **secret management** solution
|
||||||
|
2. Add **continuous security scanning** in CI/CD pipeline
|
||||||
|
3. Establish **dependency update policy**
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Run `./install-codechecks.sh` to install all required code quality tools
|
||||||
|
2. Address remaining security findings
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Ploughshares Security and Code Quality Scan Results
|
||||||
|
|
||||||
|
## Deployment Status
|
||||||
|
|
||||||
|
✅ **Application successfully deployed**
|
||||||
|
- Web application running at http://localhost:5001
|
||||||
|
- API documentation available at http://localhost:5001/api-docs
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
### Functionality Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- Core imports verified
|
||||||
|
- API routes verified
|
||||||
|
|
||||||
|
### Code Quality Tests
|
||||||
|
✅ **All tests passed**
|
||||||
|
- Python syntax valid
|
||||||
|
- No inappropriate print statements
|
||||||
|
|
||||||
|
### Dependency Tests
|
||||||
|
✅ **Basic tests passed**
|
||||||
|
- requirements.txt exists
|
||||||
|
- No known vulnerable package versions in hardcoded list
|
||||||
|
|
||||||
|
## Security Scan Results
|
||||||
|
|
||||||
|
### Dependency Vulnerabilities (Safety)
|
||||||
|
|
||||||
|
⚠️ **Multiple vulnerabilities detected in dependencies**
|
||||||
|
|
||||||
|
| Package | Installed | Affected | Issue ID |
|
||||||
|
|------------|-----------|-------------------------|----------|
|
||||||
|
| flask | 2.2.2 | <2.2.5 | 55261 |
|
||||||
|
| flask | 2.2.2 | <3.1.1 | 77323 |
|
||||||
|
| requests | 2.28.1 | <2.32.2 | 71064 |
|
||||||
|
| requests | 2.28.1 | >=2.3.0,<2.31.0 | 58755 |
|
||||||
|
| gunicorn | 20.1.0 | <21.2.0 | 72780 |
|
||||||
|
| gunicorn | 20.1.0 | <22.0.0 | 71600 |
|
||||||
|
| gunicorn | 20.1.0 | <23.0.0 | 76244 |
|
||||||
|
| werkzeug | 2.3.7 | <2.3.8 | 62019 |
|
||||||
|
| werkzeug | 2.3.7 | <3.0.3 | 71594 |
|
||||||
|
| werkzeug | 2.3.7 | <3.0.6 | 73969 |
|
||||||
|
| werkzeug | 2.3.7 | <3.0.6 | 73889 |
|
||||||
|
| werkzeug | 2.3.7 | <=2.3.7 | 71595 |
|
||||||
|
| jinja2 | 3.1.2 | <3.1.3 | 64227 |
|
||||||
|
| jinja2 | 3.1.2 | <3.1.4 | 71591 |
|
||||||
|
| jinja2 | 3.1.2 | <3.1.5 | 76378 |
|
||||||
|
| jinja2 | 3.1.2 | <3.1.5 | 74735 |
|
||||||
|
| jinja2 | 3.1.2 | <3.1.6 | 75976 |
|
||||||
|
|
||||||
|
### Code Security Issues (Bandit)
|
||||||
|
|
||||||
|
⚠️ **3 potential security issues detected**
|
||||||
|
|
||||||
|
1. **Hardcoded Password String** (Severity: Low, Confidence: Medium)
|
||||||
|
- Location: app.py:20
|
||||||
|
- Issue: `app.secret_key = 'supersecretkey'`
|
||||||
|
- CWE-259: Use of Hard-coded Password
|
||||||
|
|
||||||
|
2. **Binding to All Interfaces** (Severity: Medium, Confidence: Medium)
|
||||||
|
- Location: app.py:220
|
||||||
|
- Issue: `app.run(host='0.0.0.0', port=port)`
|
||||||
|
- CWE-605: Multiple Binds to the Same Port
|
||||||
|
|
||||||
|
3. **Hardcoded Password in Function Argument** (Severity: Low, Confidence: Medium)
|
||||||
|
- Location: init_db.py:6-11
|
||||||
|
- Issue: `password="testpass"` in database connection
|
||||||
|
- CWE-259: Use of Hard-coded Password
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Immediate Actions
|
||||||
|
1. **Update vulnerable dependencies** to their latest secure versions
|
||||||
|
2. **Replace hardcoded secrets** with environment variables
|
||||||
|
3. **Restrict network binding** in development environments
|
||||||
|
|
||||||
|
### Long-term Improvements
|
||||||
|
1. Implement **secret management** solution
|
||||||
|
2. Add **continuous security scanning** in CI/CD pipeline
|
||||||
|
3. Establish **dependency update policy**
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
1. Run `./install-codechecks.sh` to install all required code quality tools
|
||||||
|
2. Update dependencies to secure versions
|
||||||
|
3. Address security findings
|
|
@ -0,0 +1,64 @@
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
ploughshares-internal:
|
||||||
|
driver: overlay
|
||||||
|
|
||||||
|
services:
|
||||||
|
ploughshares-app:
|
||||||
|
image: 'git.nixc.us/colin/ploughshares:latest'
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.hostname == macmini14
|
||||||
|
labels:
|
||||||
|
homepage.group: tools
|
||||||
|
homepage.name: Project Ploughshares
|
||||||
|
homepage.href: https://ploughshares.nixc.us/
|
||||||
|
homepage.description: Transaction Management System
|
||||||
|
traefik.enable: "true"
|
||||||
|
|
||||||
|
traefik.http.routers.ploughshares.rule: Host(`ploughshares.nixc.us`)
|
||||||
|
traefik.http.routers.ploughshares.entrypoints: websecure
|
||||||
|
traefik.http.routers.ploughshares.tls: "true"
|
||||||
|
traefik.http.routers.ploughshares.tls.certresolver: letsencryptresolver
|
||||||
|
traefik.http.services.ploughshares.loadbalancer.server.port: 5001
|
||||||
|
traefik.docker.network: traefik
|
||||||
|
environment:
|
||||||
|
- FLASK_RUN_PORT=5001
|
||||||
|
- POSTGRES_HOST=ploughshares-db
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD=ploughshares_password
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
- ploughshares-internal
|
||||||
|
volumes:
|
||||||
|
- ploughshares_uploads:/app/uploads
|
||||||
|
depends_on:
|
||||||
|
- ploughshares-db
|
||||||
|
|
||||||
|
ploughshares-db:
|
||||||
|
image: postgres:12
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.hostname == macmini14
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD=ploughshares_password
|
||||||
|
networks:
|
||||||
|
- ploughshares-internal
|
||||||
|
volumes:
|
||||||
|
- ploughshares_db_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ploughshares_db_data:
|
||||||
|
driver: local
|
||||||
|
ploughshares_uploads:
|
||||||
|
driver: local
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
ploughshares-internal:
|
||||||
|
driver: overlay
|
||||||
|
|
||||||
|
services:
|
||||||
|
ploughshares-app:
|
||||||
|
image: 'git.nixc.us/colin/ploughshares:staging'
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
labels:
|
||||||
|
homepage.group: tools
|
||||||
|
homepage.name: Project Ploughshares (Staging)
|
||||||
|
homepage.href: https://staging-ploughshares.nixc.us/
|
||||||
|
homepage.description: Transaction Management System (Staging)
|
||||||
|
traefik.enable: "true"
|
||||||
|
|
||||||
|
traefik.http.routers.ploughshares-staging.rule: Host(`staging-ploughshares.nixc.us`)
|
||||||
|
traefik.http.routers.ploughshares-staging.entrypoints: websecure
|
||||||
|
traefik.http.routers.ploughshares-staging.tls: "true"
|
||||||
|
traefik.http.routers.ploughshares-staging.tls.certresolver: letsencryptresolver
|
||||||
|
traefik.http.services.ploughshares-staging.loadbalancer.server.port: 5001
|
||||||
|
traefik.docker.network: traefik
|
||||||
|
environment:
|
||||||
|
- FLASK_RUN_PORT=5001
|
||||||
|
- POSTGRES_HOST=ploughshares-db-staging
|
||||||
|
- POSTGRES_PORT=5432
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password_staging
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
- ploughshares-internal
|
||||||
|
volumes:
|
||||||
|
- ploughshares_uploads_staging:/app/uploads
|
||||||
|
secrets:
|
||||||
|
- db_password_staging
|
||||||
|
depends_on:
|
||||||
|
- ploughshares-db-staging
|
||||||
|
|
||||||
|
ploughshares-db-staging:
|
||||||
|
image: postgres:12
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=ploughshares
|
||||||
|
- POSTGRES_USER=ploughshares
|
||||||
|
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password_staging
|
||||||
|
networks:
|
||||||
|
- ploughshares-internal
|
||||||
|
volumes:
|
||||||
|
- ploughshares_db_data_staging:/var/lib/postgresql/data
|
||||||
|
secrets:
|
||||||
|
- db_password_staging
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ploughshares_db_data_staging:
|
||||||
|
driver: local
|
||||||
|
ploughshares_uploads_staging:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db_password_staging:
|
||||||
|
external: true
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Ploughshares Tests
|
||||||
|
|
||||||
|
This directory contains tests for the Ploughshares application, focusing on:
|
||||||
|
|
||||||
|
1. Core functionality testing
|
||||||
|
2. Code quality verification
|
||||||
|
3. Dependency vulnerability scanning
|
||||||
|
|
||||||
|
## Test Files
|
||||||
|
|
||||||
|
- `test_app.py`: Tests core application functionality
|
||||||
|
- Verifies required imports
|
||||||
|
- Checks for essential API routes
|
||||||
|
|
||||||
|
- `test_code_quality.py`: Enforces code quality standards
|
||||||
|
- Validates Python syntax
|
||||||
|
- Checks for improper use of print statements
|
||||||
|
- Verifies flake8 installation
|
||||||
|
|
||||||
|
- `test_dependencies.py`: Scans for vulnerable dependencies
|
||||||
|
- Checks requirements.txt for known vulnerable packages
|
||||||
|
- Integrates with safety tool when available
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Run all tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a specific test file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pytest test_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with verbose output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required Tools
|
||||||
|
|
||||||
|
**IMPORTANT**: The following tools are required for complete testing:
|
||||||
|
|
||||||
|
- flake8: Code style checker
|
||||||
|
- safety: Dependency vulnerability scanner
|
||||||
|
- bandit: Security issue scanner
|
||||||
|
|
||||||
|
Install these tools with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../install-codechecks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-commit Integration
|
||||||
|
|
||||||
|
These tests are automatically run by the Git pre-commit hook to ensure code quality standards are maintained before allowing commits.
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""
|
||||||
|
Ploughshares test package.
|
||||||
|
|
||||||
|
This package contains tests for:
|
||||||
|
1. Core application functionality
|
||||||
|
2. Code quality verification
|
||||||
|
3. Dependency vulnerability scanning
|
||||||
|
"""
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""
|
||||||
|
Pytest configuration file for Ploughshares tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def project_root():
|
||||||
|
"""Return the project root directory."""
|
||||||
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_dir(project_root):
|
||||||
|
"""Return the application directory."""
|
||||||
|
return os.path.join(project_root, 'docker', 'ploughshares')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def version_file(project_root):
|
||||||
|
"""Return the path to the VERSION file."""
|
||||||
|
return os.path.join(project_root, 'VERSION')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def requirements_file(app_dir):
|
||||||
|
"""Return the path to the requirements.txt file."""
|
||||||
|
return os.path.join(app_dir, 'requirements.txt')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_file(app_dir):
|
||||||
|
"""Return the path to the app.py file."""
|
||||||
|
return os.path.join(app_dir, 'app.py')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def docker_compose_file(project_root):
|
||||||
|
"""Return the path to the docker-compose.yml file."""
|
||||||
|
return os.path.join(project_root, 'docker-compose.yml')
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for core app functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
|
||||||
|
class AppFunctionalityTests(unittest.TestCase):
|
||||||
|
"""Tests to verify core functionality of the Ploughshares app."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up the test environment."""
|
||||||
|
self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
self.app_path = os.path.join(self.project_root, 'docker', 'ploughshares', 'app.py')
|
||||||
|
|
||||||
|
def test_app_imports(self):
|
||||||
|
"""Test that app.py imports all required modules."""
|
||||||
|
with open(self.app_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check for direct imports
|
||||||
|
direct_imports = [
|
||||||
|
'import os',
|
||||||
|
'import psycopg2',
|
||||||
|
'import locale'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check for from imports
|
||||||
|
from_imports = [
|
||||||
|
'from flask import',
|
||||||
|
'from datetime import'
|
||||||
|
]
|
||||||
|
|
||||||
|
for module in direct_imports:
|
||||||
|
self.assertIn(module, content,
|
||||||
|
f"app.py should have {module}")
|
||||||
|
|
||||||
|
for module in from_imports:
|
||||||
|
self.assertIn(module, content,
|
||||||
|
f"app.py should have {module}")
|
||||||
|
|
||||||
|
def test_app_routes(self):
|
||||||
|
"""Test that app.py defines all required routes."""
|
||||||
|
with open(self.app_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
required_routes = [
|
||||||
|
"@app.route('/')",
|
||||||
|
"@app.route('/api-docs')",
|
||||||
|
"@app.route('/transaction/<int:id>')",
|
||||||
|
"@app.route('/transaction/add'",
|
||||||
|
"@app.route('/transaction/<int:id>/edit'"
|
||||||
|
]
|
||||||
|
|
||||||
|
for route in required_routes:
|
||||||
|
self.assertIn(route, content,
|
||||||
|
f"app.py should define route {route}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for code quality checks.
|
||||||
|
This script runs basic code quality checks on the Python files in the project.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import subprocess
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
|
class CodeQualityTests(unittest.TestCase):
|
||||||
|
"""Tests to verify code quality standards."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up the test environment."""
|
||||||
|
self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
self.app_dir = os.path.join(self.project_root, 'docker', 'ploughshares')
|
||||||
|
self.python_files = glob.glob(os.path.join(self.app_dir, '*.py'))
|
||||||
|
|
||||||
|
# Add test files
|
||||||
|
self.python_files.extend(glob.glob(os.path.join(self.project_root, 'tests', '*.py')))
|
||||||
|
|
||||||
|
def test_python_syntax(self):
|
||||||
|
"""Test that all Python files have valid syntax."""
|
||||||
|
for py_file in self.python_files:
|
||||||
|
with self.subTest(file=py_file):
|
||||||
|
try:
|
||||||
|
with open(py_file, 'r') as f:
|
||||||
|
compile(f.read(), py_file, 'exec')
|
||||||
|
except SyntaxError as e:
|
||||||
|
self.fail(f"Syntax error in {py_file}: {str(e)}")
|
||||||
|
|
||||||
|
def test_no_print_statements(self):
|
||||||
|
"""Test that Python files don't contain print statements (except in app.py and test files)."""
|
||||||
|
for py_file in self.python_files:
|
||||||
|
# Skip app.py as it needs print statements for logging
|
||||||
|
if os.path.basename(py_file) == 'app.py':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip test files
|
||||||
|
if '/tests/' in py_file or py_file.endswith('_test.py') or py_file.endswith('test_.py'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip init_db.py as it's a setup script
|
||||||
|
if os.path.basename(py_file) == 'init_db.py':
|
||||||
|
continue
|
||||||
|
|
||||||
|
with self.subTest(file=py_file):
|
||||||
|
with open(py_file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Check for print statements (simple check, could have false positives)
|
||||||
|
self.assertNotIn('print(', content,
|
||||||
|
f"{py_file} contains print statements, use logging instead")
|
||||||
|
|
||||||
|
def test_flake8_installed(self):
|
||||||
|
"""Test if flake8 is installed and can be run."""
|
||||||
|
try:
|
||||||
|
# Check if flake8 is installed
|
||||||
|
result = subprocess.run(
|
||||||
|
["flake8", "--version"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
self.skipTest("flake8 not installed, skipping flake8 checks")
|
||||||
|
|
||||||
|
# Note: In a real test, you might run flake8 on your files
|
||||||
|
# This is just checking if flake8 is available
|
||||||
|
self.assertEqual(0, result.returncode, "flake8 should be installed")
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.skipTest("flake8 not installed, skipping flake8 checks")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to check for vulnerable dependencies.
|
||||||
|
This script scans requirements.txt for known vulnerable packages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class DependencySecurityTests(unittest.TestCase):
|
||||||
|
"""Tests to verify that dependencies don't have known vulnerabilities."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up the test environment."""
|
||||||
|
self.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
self.requirements_path = os.path.join(self.project_root, 'docker', 'ploughshares', 'requirements.txt')
|
||||||
|
|
||||||
|
def test_requirements_file_exists(self):
|
||||||
|
"""Test that requirements.txt exists."""
|
||||||
|
self.assertTrue(os.path.exists(self.requirements_path),
|
||||||
|
"requirements.txt file should exist")
|
||||||
|
|
||||||
|
def test_no_pinned_vulnerable_packages(self):
|
||||||
|
"""Test that requirements.txt doesn't contain known vulnerable package versions."""
|
||||||
|
# This is a simplified check - in production, you would use a tool like safety
|
||||||
|
with open(self.requirements_path, 'r') as f:
|
||||||
|
requirements = f.read()
|
||||||
|
|
||||||
|
# List of known vulnerable packages and versions (example)
|
||||||
|
known_vulnerable = [
|
||||||
|
"flask==0.12.0", # Example vulnerable version
|
||||||
|
"psycopg2==2.4.5", # Example vulnerable version
|
||||||
|
"werkzeug==0.11.0", # Example vulnerable version
|
||||||
|
]
|
||||||
|
|
||||||
|
for package in known_vulnerable:
|
||||||
|
self.assertNotIn(package, requirements,
|
||||||
|
f"requirements.txt contains vulnerable package: {package}")
|
||||||
|
|
||||||
|
def test_safety_check(self):
|
||||||
|
"""
|
||||||
|
Test using safety to check for vulnerabilities (if installed).
|
||||||
|
|
||||||
|
Note: This test is skipped if safety is not installed.
|
||||||
|
To install safety: pip install safety
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check if safety is installed
|
||||||
|
subprocess.run(["safety", "--version"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
check=True)
|
||||||
|
|
||||||
|
# Skip the actual test - in CI/CD you would run this
|
||||||
|
self.skipTest("Safety check skipped in test - run manually or in CI/CD")
|
||||||
|
|
||||||
|
# Example of how to run safety:
|
||||||
|
# result = subprocess.run(
|
||||||
|
# ["safety", "check", "-r", self.requirements_path, "--json"],
|
||||||
|
# stdout=subprocess.PIPE,
|
||||||
|
# stderr=subprocess.PIPE,
|
||||||
|
# text=True,
|
||||||
|
# check=False
|
||||||
|
# )
|
||||||
|
# self.assertEqual(result.returncode, 0,
|
||||||
|
# f"Safety check failed: {result.stdout}")
|
||||||
|
|
||||||
|
except (subprocess.SubprocessError, FileNotFoundError):
|
||||||
|
self.skipTest("safety not installed, skipping vulnerability check")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,2 @@
|
||||||
|
Thu Jul 3 13:33:04 EDT 2025: Version changed from 0.1.0 to 0.1.1
|
||||||
|
Thu Jul 3 13:40:53 EDT 2025: Version changed from 0.1.1 to 0.1.2
|
|
@ -0,0 +1,144 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# versionbump.sh - Script to bump version numbers in the VERSION file and update all relevant files
|
||||||
|
# Usage: ./versionbump.sh [major|minor|patch|set VERSION|--help|-h]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION_FILE="VERSION"
|
||||||
|
DOCKER_COMPOSE_FILE="docker-compose.yml"
|
||||||
|
|
||||||
|
# Function to display help information
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
Ploughshares Version Management Tool
|
||||||
|
===================================
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
./versionbump.sh [COMMAND]
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
major Bump the major version (X.y.z -> X+1.0.0)
|
||||||
|
minor Bump the minor version (x.Y.z -> x.Y+1.0)
|
||||||
|
patch Bump the patch version (x.y.Z -> x.y.Z+1)
|
||||||
|
set VERSION Set the version to a specific value (e.g., set 1.2.3)
|
||||||
|
--help, -h Display this help message
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
This script manages the version number for the Ploughshares application.
|
||||||
|
It updates the version in multiple locations:
|
||||||
|
|
||||||
|
1. The VERSION file (source of truth)
|
||||||
|
2. Docker Compose environment variables
|
||||||
|
|
||||||
|
The web UI and application will automatically read the version from the
|
||||||
|
VERSION file at the root of the repository.
|
||||||
|
|
||||||
|
After running this script, you need to rebuild and restart the application
|
||||||
|
for the changes to take effect.
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
./versionbump.sh major # 1.2.3 -> 2.0.0
|
||||||
|
./versionbump.sh minor # 1.2.3 -> 1.3.0
|
||||||
|
./versionbump.sh patch # 1.2.3 -> 1.2.4
|
||||||
|
./versionbump.sh set 1.5.0 # Set to specific version 1.5.0
|
||||||
|
|
||||||
|
VERSION HISTORY:
|
||||||
|
The script maintains a log of version changes in version_history.log
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if help is requested or no arguments provided
|
||||||
|
if [ "$1" = "--help" ] || [ "$1" = "-h" ] || [ -z "$1" ]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if VERSION file exists
|
||||||
|
if [ ! -f "$VERSION_FILE" ]; then
|
||||||
|
echo "Error: $VERSION_FILE does not exist."
|
||||||
|
echo "Run with --help for usage information."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read current version
|
||||||
|
CURRENT_VERSION=$(cat "$VERSION_FILE")
|
||||||
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
# Function to update version in all necessary files
|
||||||
|
update_version_everywhere() {
|
||||||
|
NEW_VERSION=$1
|
||||||
|
|
||||||
|
# 1. Update VERSION file
|
||||||
|
echo "$NEW_VERSION" > "$VERSION_FILE"
|
||||||
|
echo "Updated $VERSION_FILE to $NEW_VERSION"
|
||||||
|
|
||||||
|
# 2. Log the version change
|
||||||
|
echo "$(date): Version changed from $CURRENT_VERSION to $NEW_VERSION" >> version_history.log
|
||||||
|
|
||||||
|
# 3. Update version in docker-compose.yml
|
||||||
|
# Add APP_VERSION environment variable if it doesn't exist
|
||||||
|
if ! grep -q "APP_VERSION=" "$DOCKER_COMPOSE_FILE"; then
|
||||||
|
# Find the environment section for the app service
|
||||||
|
LINE_NUM=$(grep -n "environment:" "$DOCKER_COMPOSE_FILE" | head -1 | cut -d: -f1)
|
||||||
|
if [ -n "$LINE_NUM" ]; then
|
||||||
|
# Insert APP_VERSION after the environment line
|
||||||
|
sed -i.bak "${LINE_NUM}a\\ - APP_VERSION=$NEW_VERSION" "$DOCKER_COMPOSE_FILE"
|
||||||
|
echo "Added APP_VERSION=$NEW_VERSION to $DOCKER_COMPOSE_FILE"
|
||||||
|
else
|
||||||
|
echo "Warning: Could not find environment section in $DOCKER_COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Update existing APP_VERSION
|
||||||
|
sed -i.bak "s/APP_VERSION=.*/APP_VERSION=$NEW_VERSION/" "$DOCKER_COMPOSE_FILE"
|
||||||
|
echo "Updated APP_VERSION in $DOCKER_COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
rm -f "$DOCKER_COMPOSE_FILE.bak"
|
||||||
|
|
||||||
|
echo "Version update complete! New version: $NEW_VERSION"
|
||||||
|
echo "Remember to rebuild and restart the application for changes to take effect."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process command
|
||||||
|
if [ "$1" = "set" ]; then
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "Error: No version specified. Usage: $0 set VERSION"
|
||||||
|
echo "Run with --help for usage information."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEW_VERSION="$2"
|
||||||
|
echo "Setting version to: $NEW_VERSION"
|
||||||
|
update_version_everywhere "$NEW_VERSION"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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 "Error: Unknown command '$1'"
|
||||||
|
echo "Run with --help for usage information."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Create new version string
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
echo "Bumping to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Update version in all necessary files
|
||||||
|
update_version_everywhere "$NEW_VERSION"
|
Loading…
Reference in New Issue