A management portal for Solberg Honung, handling beekeeping (Andelsbiodling) and lamb (Lammandel) shares, bookings, and automated invoicing.
- Season Management: Track and manage different production years.
- Booking Imports: Automatically import bookings from Google Sheets using the Google Sheets API.
- Invoice System: Generate unique invoices and send them automatically via email.
- Administration: Secure login via GitHub OAuth.
- Data Processing: Uses Pandas for robust handling of imported sheet data.
- Development Environment: Fully containerized with Docker and MailCatcher for email testing.
- Backend: Flask, SQLAlchemy, Flask-Migrate
- Data: Pandas, SQLite
- Integrations: Google Sheets API, GitHub OAuth
- Frontend: Bootstrap 5, Jinja2 Templates
- DevOps: Docker, Docker Compose, MailCatcher
-
Clone the repository:
git clone https://github.com/valyo/sh-portal cd sh_portal -
Environment Configuration: Create a
.envfile in the root directory with the following variables:SECRET_KEY=your-secret-key DATABASE_URL=sqlite:///instance/sh.db GITHUB_CLIENT_ID=your-github-id GITHUB_CLIENT_SECRET=your-github-secret GOOGLE_SHEET_ID=your-sheet-id
Place your Google Service Account JSON key (
sh-web-portal-f370fff1378a.json) in the root directory. -
Build and run using Docker Compose:
docker-compose up --build
After changing
requirements.txt(e.g. bumping Flask), rebuild the image so new dependencies are installed:docker-compose build --no-cache backend && docker-compose up -
Access the application:
- Web Portal: http://localhost:8087
- MailCatcher (Email testing): http://localhost:1088
.
├── main.py # Application entry point
├── migrations.py # Database migration script
├── requirements.txt # Python dependencies
├── Dockerfile # Container configuration
├── docker-compose.yml # Multi-container orchestration
├── .env # Environment variables (to be created)
├── sh-web-portal-f370fff1378a.json # Google Service Account key
├── sh_portal/ # Main application package
│ ├── __init__.py # App factory and configuration
│ ├── models.py # SQLAlchemy database models
│ ├── utils.py # Utility functions (Sheet imports, etc.)
│ ├── home.py # Main portal blueprint
│ ├── seasons.py # Season management blueprint
│ ├── andelsbiodling.py # Beekeeping blueprint
│ ├── lammandel.py # Lamb blueprint
│ ├── commands.py # Custom Flask CLI commands
│ ├── static/ # CSS, JS, and Images
│ └── templates/ # HTML templates
├── migrations/ # Database migration history
└── instance/ # SQLite database storage
A helper script develop_pdf.py is available to preview certificate templates locally without running the full portal. It watches the template files and regenerates a PDF preview instantly on every save.
-
Create and activate a virtual environment:
python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate
-
Install dependencies:
pip install weasyprint watchdog jinja2
-
Run the preview script:
python develop_pdf.py
This will generate
certificate_preview.pdfin the root directory and update it whenever you change the HTML template sh_portal/templates/certificate_pdf_template.html
Tests use pytest and cover the mail backend selection (cookie vs config, connection params, API).
-
Install test dependencies (in the same environment as the app):
pip install -r requirements.txt -r requirements-dev.txt
-
Run tests (use
python -m pytestso the venv’s Python is used, not a global/pyenv pytest):python -m pytest
Or with coverage:
python -m pytest --cov=sh_portal --cov-report=term-missingTo run only mail-related tests:
python -m pytest tests/test_mail_utils.py tests/test_mail_backend_api.py -v
To create an initial admin user for GitHub OAuth login, you can use the custom Flask command:
docker-compose exec backend flask create-new-admin <github_username>Deploy using the production Docker Compose file and a pre-built image from GitHub Container Registry.
Prerequisites: Docker and Docker Compose on the server; access to ghcr.io.
-
Create the deployment directory on your server:
mkdir -p /opt/sh-portal cd /opt/sh-portal -
Copy the production compose file to your server:
# From your local machine scp docker-compose.prod.yml user@your-server:/opt/sh-portal/docker-compose.yml -
Create the data directories:
mkdir -p data/instance data/invoices data/temp data/credentials
-
Create a
.envfile (edit values for your environment):cat > .env << 'EOF' # Docker image configuration GITHUB_REPO=your-username/sh_portal_v0.2 VERSION=latest # App port (external) PORT=8087 # Flask secret (change in production) SECRET_KEY=your-secret-key # GitHub OAuth (for app login) GITHUB_CLIENT_ID=your-github-oauth-app-client-id GITHUB_CLIENT_SECRET=your-github-oauth-app-client-secret OAUTH_REDIRECT_URI=https://your-domain.com/callback # Optional: Google Sheets import # GOOGLE_SHEET_ID=your-google-sheet-id # Mail configuration MAIL_SERVER=smtp.gmail.com MAIL_PORT=587 MAIL_USE_TLS=True MAIL_USE_SSL=False MAIL_USERNAME=your-email@gmail.com MAIL_PASSWORD=your-app-password MAIL_DEFAULT_SENDER=your-email@gmail.com EOF
-
Copy your Google Sheets credentials (if using Google Sheets import):
cp /path/to/your/service-account.json data/credentials/service-account.json
-
Log in to GitHub Container Registry:
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
-
Start the application:
docker compose up -d
-
Run database migrations (first deploy or after updating):
docker compose run --rm --entrypoint "" app flask db upgrade
/opt/sh-portal/
├── docker-compose.yml # The compose file
├── .env # Environment variables
└── data/
├── instance/ # SQLite database (sh.db)
├── invoices/ # Generated invoice PDFs
├── temp/ # Generated certificate PDFs
└── credentials/ # Google Sheets service account JSON
docker compose pull
docker compose run --rm --entrypoint "" app flask db upgrade
docker compose up -dTo pin a version, set VERSION=0.2.0 (or desired tag) in .env and run docker compose up -d.
# Create a backup
tar -czvf sh-portal-backup-$(date +%Y%m%d).tar.gz data/
# Restore
tar -xzvf sh-portal-backup-YYYYMMDD.tar.gzdocker compose logs -f
docker compose logs --tail 100
docker compose psserver {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:8087;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}