diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..784f5ed
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,58 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+*.egg-info/
+dist/
+build/
+*.egg
+
+# Virtual environments
+.venv/
+venv/
+ENV/
+env/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Git
+.git/
+.gitignore
+.gitattributes
+
+# Docker
+Dockerfile
+.dockerignore
+docker-compose*.yml
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+tests/
+
+# Output files
+_out_/
+
+# Documentation
+*.md
+!README.md
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Private config files
+PrivateConfig.py
+Config_Sample.py
+
+# Firebase certificates (should be mounted as secrets)
+*.json
+!resources/editions/data_sample.json
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..0153bf3
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,34 @@
+# Environment variables for IdentificationGenerator
+# Copy this file to .env and update with your actual values
+
+# ===== Edition Configuration =====
+EDITION=2025
+TEST=False
+
+# ===== Database Configuration =====
+DB_PATH_T=hackeps-2025/dev/users
+DB_PATH=hackeps-2025/prod/users
+
+# ===== API Configuration =====
+# Backend API base URL
+BASE_URL=http://localhost:8000
+# Service authentication token (KEEP SECRET!)
+SERVICE_TOKEN=your_service_token_here
+
+# ===== Optional Path Overrides =====
+# Uncomment and modify if you need custom paths
+# EDITIONS_FOLDER=editions
+# OUT_FOLDER=_out_
+# RES_FOLDER=resources
+# FONT_FOLDER=fonts
+
+# ===== Optional File Name Overrides =====
+# DATA_FILE=data.json
+# DB_CERT=2019_firebase_cert.json
+# FONT_FILE=SpaceMono-Regular.ttf
+# BOLD_FONT_FILE=SpaceMono-Bold.ttf
+
+# ===== Template File Overrides =====
+# BAK_FILE_CONTESTANT=plantilles/participant.png
+# BAK_FILE_STAFF=plantilles/organitzador.png
+# BAK_FILE_EMPRESA=plantilles/patrocinador.png
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
new file mode 100644
index 0000000..74cfd84
--- /dev/null
+++ b/.github/workflows/README.md
@@ -0,0 +1,13 @@
+# GitHub Actions Workflows
+
+This directory will contain GitHub Actions workflows for CI/CD.
+
+## Planned Workflows
+
+- **CI/CD Pipeline**: Build, test, and deploy the application
+- **Docker Build**: Automated Docker image building and pushing to registry
+- **Testing**: Automated test runs on pull requests
+
+## Coming Soon
+
+Deployment workflows will be added here in the future.
diff --git a/.gitignore b/.gitignore
index 63edd56..a83e533 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,110 +1,118 @@
-#custom
-resources/*_firebase_cert.json
-resources/editions/*/data.json
-_out_/
-*.pdf
-
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-.hypothesis/
-.pytest_cache/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# pyenv
-.python-version
-
-# celery beat schedule file
-celerybeat-schedule
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
+#custom
+resources/*_firebase_cert.json
+resources/editions/*/data.json
+_out_/
+generated_cards/
+*.pdf
+PrivateConfig.py
+Config.py
+.idea/*
+.vscode/*
+# Docker
+docker-compose.override.yml
+.env
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 0e40fe8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-
-# Default ignored files
-/workspace.xml
\ No newline at end of file
diff --git a/.idea/IdentificationGenerator.iml b/.idea/IdentificationGenerator.iml
deleted file mode 100644
index 6711606..0000000
--- a/.idea/IdentificationGenerator.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index d4fc832..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index e59bda9..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index de19f71..0000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- // Use IntelliSense para saber los atributos posibles.
- // Mantenga el puntero para ver las descripciones de los existentes atributos.
- // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Python: Archivo actual",
- "type": "python",
- "request": "launch",
- "program": "python main.py",
- "console": "integratedTerminal"
- }
- ]
-}
\ No newline at end of file
diff --git a/Config.py b/Config.py
deleted file mode 100644
index 931a170..0000000
--- a/Config.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from os import path
-from PIL import ImageFont
-
-TEST = False
-
-EDITION='2022'
-EDITIONS_FOLDER = 'editions'
-
-DB_PATH_T = 'hackeps-'+EDITION+'/dev/users'
-DB_PATH = 'hackeps-'+EDITION+'/prod/users'
-
-
-OUT_FOLDER = '_out_'
-OUT_PATH = path.join('.', OUT_FOLDER)
-
-RES_FOLDER = 'resources'
-RES_PATH = path.join('.', RES_FOLDER)
-
-DATA_FILE = 'data.json'
-DATA_PATH = path.join(RES_PATH, EDITIONS_FOLDER, EDITION, DATA_FILE)
-
-DB_CERT = '2019_firebase_cert.json'
-DB_CERT_PATH = path.join(RES_PATH, DB_CERT)
-
-BAK_FILE = 'plantilla.png'
-BAK_PATH = path.join(RES_PATH, EDITIONS_FOLDER, EDITION, BAK_FILE)
-
-FONT_FOLDER = 'fonts'
-FONT_FILE = 'SpaceMono-Bold.ttf'
-FONT_PATH = path.join(RES_PATH, FONT_FOLDER, FONT_FILE)
-TYPE_FONT_SIZE = 50
-NAME_FONT_SIZE = 25
-FONT_COLOR = (0,0,0)
-WHITE_FONT_COLOR = (255,255,255)
-TYPE_FONT = ImageFont.truetype(FONT_PATH, TYPE_FONT_SIZE)
-NAME_FONT = ImageFont.truetype(FONT_PATH, NAME_FONT_SIZE)
-# FONT = ImageFont.truetype("Symbola.ttf", 60, encoding='unic')
-
-MAIN_COLOR = (31, 33, 36)
-BAK_COLOR = (247, 247, 242)
\ No newline at end of file
diff --git a/DOCKER.md b/DOCKER.md
new file mode 100644
index 0000000..6daa735
--- /dev/null
+++ b/DOCKER.md
@@ -0,0 +1,218 @@
+# Docker Quick Start Guide
+
+This guide will help you get the IdentificationGenerator API up and running with Docker.
+
+## Prerequisites
+
+- Docker installed ([Get Docker](https://docs.docker.com/get-docker/))
+- Docker Compose installed (usually comes with Docker Desktop)
+- Git (to clone the repository)
+
+## Quick Start
+
+### 1. Setup Environment
+
+```bash
+# Copy the example environment file
+cp .env.example .env
+
+# Edit .env if needed (optional for basic testing)
+# nano .env
+```
+
+### 2. Start the Application
+
+**Option A: Using Make (Recommended)**
+```bash
+# View available commands
+make help
+
+# Start in production mode (background)
+make up
+
+# Or start in development mode (with hot-reload)
+make dev
+```
+
+**Option B: Using Docker Compose directly**
+```bash
+# Production mode
+docker-compose up -d
+
+# Development mode
+docker-compose -f docker-compose.dev.yml up
+```
+
+### 3. Verify It's Running
+
+Open your browser and visit:
+- API: http://localhost:8000
+- Interactive Docs: http://localhost:8000/docs
+- Alternative Docs: http://localhost:8000/redoc
+
+You should see the API documentation and be able to test endpoints.
+
+### 4. View Logs
+
+```bash
+# Using Make
+make logs
+
+# Using Docker Compose
+docker-compose logs -f
+```
+
+## Common Tasks
+
+### Access Container Shell
+
+```bash
+# Using Make
+make shell
+
+# Using Docker Compose
+docker exec -it identification-generator-api /bin/bash
+```
+
+### Run Tests
+
+```bash
+# Using Make
+make test
+
+# Using Docker Compose
+docker-compose exec api uv run pytest
+```
+
+### Stop the Application
+
+```bash
+# Using Make
+make down
+
+# Using Docker Compose
+docker-compose down
+```
+
+### Rebuild After Code Changes
+
+```bash
+# Using Make
+make rebuild
+
+# Using Docker Compose
+docker-compose up -d --build
+```
+
+## Development Workflow
+
+### Hot-Reload Development
+
+1. Start in development mode:
+ ```bash
+ make dev
+ ```
+
+2. Edit your code - changes will automatically reload
+
+3. View logs in real-time to see the reload happening
+
+### Running Tests
+
+```bash
+# Run all tests
+make test
+
+# Run specific test file
+docker-compose exec api uv run pytest tests/test_specific.py
+
+# Run with verbose output
+docker-compose exec api uv run pytest -v
+```
+
+## Troubleshooting
+
+### Port Already in Use
+
+If port 8000 is already in use:
+
+1. Edit `docker-compose.yml`
+2. Change `"8000:8000"` to `"8080:8000"` (or another available port)
+3. Access the API at http://localhost:8080
+
+### Container Won't Start
+
+```bash
+# Check logs
+make logs
+
+# Common issues:
+# 1. Missing dependencies - rebuild the image
+make rebuild
+
+# 2. Permission issues - check file permissions
+ls -la
+
+# 3. Port conflicts - change the port in docker-compose.yml
+```
+
+### Generated Files Not Appearing
+
+The generated ID cards are saved to `./generated_cards/` on your host machine.
+
+If files aren't appearing:
+1. Check if the directory exists: `ls -la generated_cards/`
+2. Check container logs for errors: `make logs`
+3. Verify volume mount in docker-compose.yml
+
+### Firebase Connection Issues
+
+If you see Firebase-related errors:
+
+1. Ensure you have the Firebase credentials file
+2. Place it at `resources/2019_firebase_cert.json`
+3. Uncomment the volume mount in `docker-compose.yml`:
+ ```yaml
+ - ./resources/2019_firebase_cert.json:/app/resources/2019_firebase_cert.json:ro
+ ```
+4. Restart: `make restart`
+
+## File Structure
+
+```
+IdentificationGenerator/
+├── docker-compose.yml # Production configuration
+├── docker-compose.dev.yml # Development configuration
+├── Dockerfile # Production image
+├── Dockerfile.dev # Development image
+├── .dockerignore # Files to exclude from build
+├── .env.example # Environment template
+├── .env # Your environment (git-ignored)
+├── Makefile # Convenient commands
+├── generated_cards/ # Output directory (created automatically)
+└── resources/ # Templates, fonts, etc.
+```
+
+## Next Steps
+
+- Read the main [README.md](README.md) for API documentation
+- Check out the interactive API docs at http://localhost:8000/docs
+- Explore the different endpoints for contestants, mentors, etc.
+- Set up your Firebase credentials for full functionality
+
+## Getting Help
+
+- Check the logs: `make logs`
+- View container status: `docker-compose ps`
+- Access the shell for debugging: `make shell`
+- Review the main README.md for more details
+
+## Clean Up
+
+To completely remove all containers, images, and generated files:
+
+```bash
+make clean
+```
+
+**Warning:** This will delete all generated ID cards and Docker resources!
diff --git a/DOCKERIZATION_SUMMARY.md b/DOCKERIZATION_SUMMARY.md
new file mode 100644
index 0000000..35e9da7
--- /dev/null
+++ b/DOCKERIZATION_SUMMARY.md
@@ -0,0 +1,155 @@
+# Dockerization Summary
+
+## What Was Created
+
+This document summarizes all the Docker-related files added to the IdentificationGenerator project.
+
+### Core Docker Files
+
+1. **Dockerfile** - Production-ready Docker image
+ - Uses Python 3.12 slim base image
+ - Installs system dependencies (Cairo, Pango for SVG/PDF rendering)
+ - Uses `uv` for fast dependency management
+ - Includes health check
+ - Runs with uvicorn on port 8000
+
+2. **Dockerfile.dev** - Development Docker image
+ - Same as production but includes dev dependencies
+ - Enables hot-reload for development
+ - Optimized for local development workflow
+
+3. **docker-compose.yml** - Production orchestration
+ - Defines the API service
+ - Sets up volume mounts for generated files
+ - Configures networking
+ - Includes commented nginx configuration for future use
+
+4. **docker-compose.dev.yml** - Development orchestration
+ - Mounts source code for hot-reload
+ - Enables interactive debugging
+ - Uses development Dockerfile
+
+5. **.dockerignore** - Build optimization
+ - Excludes unnecessary files from Docker build context
+ - Reduces image size and build time
+
+### Configuration Files
+
+6. **.env.example** - Environment variable template
+ - Shows required environment variables
+ - Safe to commit (no secrets)
+
+7. **docker-compose.override.yml.example** - Local customization template
+ - Shows how to customize Docker setup locally
+ - Examples for ports, volumes, resources
+
+### Documentation
+
+8. **README.md** - Updated with comprehensive Docker instructions
+ - Quick start guide
+ - Docker commands
+ - API endpoints
+ - Troubleshooting
+
+9. **DOCKER.md** - Detailed Docker guide
+ - Step-by-step setup
+ - Development workflows
+ - Common tasks
+ - Troubleshooting section
+
+10. **Makefile** - Convenient command shortcuts
+ - `make help` - Show available commands
+ - `make up` - Start production
+ - `make dev` - Start development
+ - `make test` - Run tests
+ - And more...
+
+### Other Updates
+
+11. **.gitignore** - Updated to exclude:
+ - `generated_cards/` directory
+ - `docker-compose.override.yml`
+ - `.env` file
+
+12. **.github/workflows/README.md** - Placeholder for future CI/CD
+ - Reserved for GitHub Actions workflows
+ - Will be implemented later
+
+## Key Features
+
+### Production Ready
+- ✅ Optimized Docker image with multi-stage potential
+- ✅ Health checks configured
+- ✅ Proper environment variable handling
+- ✅ Volume mounts for persistent data
+- ✅ Resource limits ready to configure
+
+### Developer Friendly
+- ✅ Hot-reload in development mode
+- ✅ Easy-to-use Makefile commands
+- ✅ Separate dev/prod configurations
+- ✅ Interactive debugging support
+- ✅ Comprehensive documentation
+
+### Best Practices
+- ✅ Uses `uv` for fast dependency management
+- ✅ Minimal base image (Python 3.12 slim)
+- ✅ .dockerignore for efficient builds
+- ✅ Proper .gitignore entries
+- ✅ Environment variable configuration
+- ✅ Volume mounts for generated files
+
+## Quick Start
+
+```bash
+# 1. Setup
+cp .env.example .env
+
+# 2. Start (choose one)
+make up # Production
+make dev # Development
+
+# 3. Access
+# http://localhost:8000/docs
+```
+
+## File Tree
+
+```
+IdentificationGenerator/
+├── .dockerignore
+├── .env.example
+├── .github/
+│ └── workflows/
+│ └── README.md
+├── .gitignore (updated)
+├── docker-compose.dev.yml
+├── docker-compose.override.yml.example
+├── docker-compose.yml
+├── DOCKER.md
+├── Dockerfile
+├── Dockerfile.dev
+├── Makefile
+└── README.md (updated)
+```
+
+## Next Steps
+
+1. **Test the setup**: Run `make up` to verify everything works
+2. **Customize**: Copy override example if needed
+3. **GitHub Actions**: Add CI/CD workflows when ready
+4. **Production**: Configure nginx, SSL, and secrets management
+
+## Notes
+
+- GitHub Actions deployment workflows are planned but not yet implemented
+- Firebase credentials should be mounted as secrets in production
+- The `generated_cards/` directory is created automatically
+- All Docker-related files are properly git-ignored
+
+## Support
+
+For issues or questions:
+- Check DOCKER.md for troubleshooting
+- Review README.md for API documentation
+- Run `make help` for available commands
diff --git a/Dockerfile b/Dockerfile
index 30c1b19..6062a78 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,48 @@
-FROM python:3.10
-ENV PYTHONUNBUFFERED=1
-WORKDIR /app
-
-RUN pip install --no-cache-dir -U pip
-
-COPY requirements.txt /tmp/requirements.txt
-RUN pip install --no-cache-dir -r /tmp/requirements.txt
-
-COPY . /app
+# Use Python 3.12 slim image for smaller size
+FROM python:3.12-slim
+
+# Set environment variables
+ENV PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ UV_SYSTEM_PYTHON=1
+
+# Set working directory
+WORKDIR /app
+
+# Install system dependencies required for the project
+# - cairo for cairosvg
+# - fonts for text rendering
+RUN apt-get update && apt-get install -y \
+ libcairo2 \
+ libpango-1.0-0 \
+ libpangocairo-1.0-0 \
+ libgdk-pixbuf2.0-0 \
+ libffi-dev \
+ shared-mime-info \
+ fonts-dejavu-core \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv for fast dependency management
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+
+# Copy dependency files
+COPY pyproject.toml uv.lock ./
+
+# Install dependencies using uv
+RUN uv sync --frozen --no-dev
+
+# Copy application code
+COPY . .
+
+# Create output directory
+RUN mkdir -p _out_
+
+# Expose port for FastAPI
+EXPOSE 8000
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD python -c "import requests; requests.get('http://localhost:8000/')" || exit 1
+
+# Run the application with uvicorn
+CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/Dockerfile.dev b/Dockerfile.dev
new file mode 100644
index 0000000..1356457
--- /dev/null
+++ b/Dockerfile.dev
@@ -0,0 +1,42 @@
+# Development Dockerfile with hot-reload support
+FROM python:3.12-slim
+
+# Set environment variables
+ENV PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ UV_SYSTEM_PYTHON=1
+
+# Set working directory
+WORKDIR /app
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ libcairo2 \
+ libpango-1.0-0 \
+ libpangocairo-1.0-0 \
+ libgdk-pixbuf2.0-0 \
+ libffi-dev \
+ shared-mime-info \
+ fonts-dejavu-core \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+
+# Copy dependency files
+COPY pyproject.toml uv.lock ./
+
+# Install dependencies including dev dependencies
+RUN uv sync --frozen
+
+# Copy application code (will be overridden by volume mount in dev)
+COPY . .
+
+# Create output directory
+RUN mkdir -p _out_
+
+# Expose port
+EXPOSE 8000
+
+# Run with hot-reload enabled
+CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b0ab552
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,69 @@
+.PHONY: help build up down logs shell test clean dev prod restart
+
+# Default target
+help:
+ @echo "Available commands:"
+ @echo " make build - Build Docker images"
+ @echo " make up - Start containers in production mode"
+ @echo " make down - Stop and remove containers"
+ @echo " make logs - View container logs"
+ @echo " make shell - Access container shell"
+ @echo " make test - Run tests in container"
+ @echo " make clean - Remove containers, images, and volumes"
+ @echo " make dev - Start in development mode with hot-reload"
+ @echo " make prod - Start in production mode"
+ @echo " make restart - Restart containers"
+
+# Build Docker images
+build:
+ docker-compose build
+
+# Start containers (production)
+up:
+ docker-compose up -d
+
+# Start containers (production, foreground)
+prod:
+ docker-compose up
+
+# Start containers (development with hot-reload)
+dev:
+ docker-compose -f docker-compose.dev.yml up
+
+# Stop containers
+down:
+ docker-compose down
+
+# View logs
+logs:
+ docker-compose logs -f
+
+# Access container shell
+shell:
+ docker exec -it identification-generator-api /bin/bash
+
+# Run tests
+test:
+ docker-compose exec api uv run pytest
+
+# Restart containers
+restart:
+ docker-compose restart
+
+# Clean everything (containers, images, volumes)
+clean:
+ docker-compose down -v --rmi all
+ rm -rf generated_cards/*
+
+# Rebuild and restart
+rebuild: down build up
+
+# Check container status
+status:
+ docker-compose ps
+
+# View API documentation
+docs:
+ @echo "API Documentation available at:"
+ @echo " - Swagger UI: http://localhost:8000/docs"
+ @echo " - ReDoc: http://localhost:8000/redoc"
diff --git a/Model.py b/Model.py
deleted file mode 100644
index ab80de4..0000000
--- a/Model.py
+++ /dev/null
@@ -1,269 +0,0 @@
-import os
-
-import firebase_admin
-from PIL import Image
-from firebase_admin import firestore
-
-import Config
-import Tools
-
-
-class Card:
- QR_PIX_SIZE = 1
- QR_POS = (450, 78)
- _QR_SIZE = 267
- QR_SIZE = (_QR_SIZE, _QR_SIZE)
- QR_BORDER_SIZE = 0
- TYPE_POS = (0, 475)
- NAME_POS = (QR_POS[0], 370)
- NICK_POS = (1198, 870)
-
-
-class Assistant(object):
- __ID:int = 1
- __DATA:str = 'empty'
- _DATA_FILE:str = Config.DATA_PATH
-
- def __init__(self, id:str, type:str='', name:str=None):
- self.id = id
- self.type = type
- self.name = name
- self.card = None
- self.qr = None
-
- def show(self):
- self.card.show()
-
- def save(self):
- self.card.save(os.path.join(Config.OUT_PATH, str(self.id) + '.png'))
-
- def generate_qr(self, crypt_id=False):
- self.qr = Tools.generate_qr(self.id, Card.QR_PIX_SIZE, Card.QR_BORDER_SIZE)
- self.qr = Tools.scale(self.qr, Card.QR_SIZE, False)
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- self.card = Image.open(Config.BAK_PATH)
- Tools.draw_text(self.card, self.type, Card.TYPE_POS, Config.TYPE_FONT, Config.WHITE_FONT_COLOR)
- if self.type == '':
- self.smallen()
-
- def smallen(self):
- blank = Image.new('RGB', (1082, 782),(255,255,255))
- blank.paste(self.card, (0, 0))
- self.card = blank
-
- @staticmethod
- def get_data():
- data = Tools.DataFile.get_content(Assistant._DATA_FILE, 'JSON')
- num = data[Assistant.__DATA]
- res = []
- for _ in range(num):
- res.append(Assistant('A' + str(Assistant.__ID)))
- Assistant.__ID += 1
- if Config.TEST:
- break
- return res
-
-
-class Guest(Assistant):
- __ID:int = 1
- __TYPE:str = 'CONVIDAT'
- __DATA:str = 'guests'
-
- def __init__(self, name:str, mtype:str='', logo:str='', has_qr:bool=False):
- super().__init__('HackEPS_Guest_' + str(Guest.__ID), (mtype, Guest.__TYPE)[mtype == ''], name)
- Guest.__ID += 1
- self.has_qr = has_qr
- self.logo = logo
- if has_qr:
- self.generate_qr(True)
- if not logo == '':
- self.logopath = os.path.join(Config.RES_PATH, Config.EDITIONS_FOLDER, Config.EDITION, 'images', logo)
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- super().generate_card(rgb_back)
- if self.logo != '':
- logo = Image.open(self.logopath).convert("RGBA")
- logo = Tools.scale(logo, Card.QR_SIZE)
- self.card.paste(logo, Card.QR_POS)
- elif self.has_qr:
- self.card.paste(self.qr, Card.QR_POS)
- if self.name != '':
- Tools.centrate_text_relative(self.card, self.name,Config.NAME_FONT, Card.NAME_POS, Card.QR_SIZE)
- self.smallen()
-
- @staticmethod
- def get_data(name=None):
- res = []
- data = Tools.DataFile.get_content(Guest._DATA_FILE, 'JSON')
- for u in data[Guest.__DATA]:
- if name is None or u['name'] == name:
- res.append(Guest(u['name'], u['type'], u['logo'], u['qr']))
- if Config.TEST or (name is not None and u['name'] == name):
- break
- return res
-
-
-class Company(Assistant):
- __ID:int = 1
- __TYPE:str = 'EMPRESA'
- __DATA:str = 'companies'
-
- def __init__(self, name:str, image):
- super().__init__('C' + str(Company.__ID), Company.__TYPE, name)
- Company.__ID += 1
- self.logopath = os.path.join(Config.RES_PATH, Config.EDITIONS_FOLDER, Config.EDITION, 'images', image)
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- super().generate_card(rgb_back)
- image = Image.open(self.logopath).convert("RGBA") # .resize((550,350), Image.ANTIALIAS)
- image = Tools.scale(image, Card.QR_SIZE)
- self.card.paste(image, Card.QR_POS)
- Tools.centrate_text_relative(self.card, self.name,Config.NAME_FONT, Card.NAME_POS, Card.QR_SIZE)
- self.smallen()
-
- @staticmethod
- def get_data(name=None):
- res = []
- data = Tools.DataFile.get_content(Company._DATA_FILE, 'JSON')
- for u in data[Company.__DATA]:
- for _ in range(u['number_of_cards']):
- if name is None or u['name'] == name:
- res.append(Company(u['name'], u['logo']))
- if Config.TEST:
- break
- if Config.TEST or (name is not None and u['name'] == name):
- break
- return res
-
-
-class Volunteer(Assistant):
- __ID:int = 1
- __LOGO_PATH:str = os.path.join(Config.RES_PATH, 'editions', Config.EDITION, 'images', 'logogran.png')
- __TYPE:str = 'VOLUNTARI/A'
- __DATA:str = 'volunteers'
-
- def __init__(self, name:str):
- super().__init__('V' + str(Volunteer.__ID), Volunteer.__TYPE, name)
- Volunteer.__ID += 1
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- super().generate_card(rgb_back)
- image = Image.open(Volunteer.__LOGO_PATH).convert("RGBA")
- image = Tools.scale(image, Card.QR_SIZE)
- self.card.paste(image, Card.QR_POS)
- Tools.centrate_text_relative(self.card, self.name,Config.NAME_FONT, Card.NAME_POS, Card.QR_SIZE)
- self.smallen()
-
- @staticmethod
- def get_data(name=None):
- res = []
- data = Tools.DataFile.get_content(Volunteer._DATA_FILE, 'JSON')
- for u in data[Volunteer.__DATA]:
- if name is None or name == u['name']:
- res.append(Volunteer(u['name']))
- if Config.TEST or (name is not None and name == u['name']):
- break
- return res
-
-class Mentor(Assistant):
- __ID:int = 1
- __LOGO_PATH:str = os.path.join(Config.RES_PATH, 'editions', Config.EDITION, 'images', 'logogran.png')
- __TYPE:str = 'MENTOR/A'
- __DATA:str = 'mentors'
-
- def __init__(self, name:str):
- super().__init__('M' + str(Mentor.__ID), Mentor.__TYPE, name)
- Mentor.__ID += 1
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- super().generate_card(rgb_back)
- image = Image.open(Mentor.__LOGO_PATH).convert("RGBA")
- image = Tools.scale(image, Card.QR_SIZE)
- self.card.paste(image, Card.QR_POS)
- Tools.centrate_text_relative(self.card, self.name,Config.NAME_FONT, Card.NAME_POS, Card.QR_SIZE)
- self.smallen()
-
- @staticmethod
- def get_data(name=None):
- res = []
- data = Tools.DataFile.get_content(Mentor._DATA_FILE, 'JSON')
- for u in data[Mentor.__DATA]:
- if name is None or name == u['name']:
- res.append(Mentor(u['name']))
- if Config.TEST or (name is not None and name == u['name']):
- break
- return res
-
-class Organizer(Assistant):
- __ID:int = 1
- __LOGO_PATH:str = os.path.join(Config.RES_PATH, 'editions', Config.EDITION, 'images', 'logogran.png')
- __TYPE:str = 'ORGANIZACIÓ'
- __DATA:str = 'organizers'
-
- def __init__(self, name):
- super().__init__('O' + str(Organizer.__ID), Organizer.__TYPE, name)
- Organizer.__ID += 1
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- super().generate_card(rgb_back)
- image = Image.open(Organizer.__LOGO_PATH).convert("RGBA")
- image = Tools.scale(image, Card.QR_SIZE)
- self.card.paste(image, Card.QR_POS)
- Tools.centrate_text_relative(self.card, self.name,Config.NAME_FONT, Card.NAME_POS, Card.QR_SIZE)
- # Tools.draw_text(self.card, self.name, Card.NAME_POS, Config.NAME_FONT, Config.WHITE_FONT_COLOR, False)
- self.smallen()
-
- @staticmethod
- def get_data(name=None):
- res = []
- print(Organizer._DATA_FILE)
- data = Tools.DataFile.get_content(Organizer._DATA_FILE, 'JSON')
- for u in data[Organizer.__DATA]:
- if name is None or u['name'] == name:
- res.append(Organizer(u['name']))
- if Config.TEST or (name is not None and u['name'] == name):
- break
- return res
-
-
-class Contestant(Assistant):
- __CRYPT_ID = False
- __TYPE:str = 'HACKER'
- __FIREBASE = None
- __FIRE_PATH:str = Config.DB_PATH_T if Config.TEST else Config.DB_PATH
-
- def __init__(self, id, data):
- super().__init__(id, Contestant.__TYPE)
- self.generate_qr()
- self.name = data['fullName']
- self.nick = '\"' + data['nickname'] + '\"'
- if Config.TEST:
- Contestant.__FIRE_PATH = Config.DB_PATH_T
-
- def generate_card(self, rgb_back=(255, 255, 255)):
- super().generate_card(rgb_back)
- self.card.paste(self.qr, Card.QR_POS)
- Tools.centrate_text_relative(self.card, self.name,Config.NAME_FONT, Card.NAME_POS, Card.QR_SIZE)
- self.smallen()
-
- @staticmethod
- def __firebase_init(cred):
- if Contestant.__FIREBASE is None:
- Contestant.__FIREBASE = firebase_admin.initialize_app(cred)
- return Contestant.__FIREBASE
-
- @staticmethod
- def get_data(id=None, name=None):
- cred = firebase_admin.credentials.Certificate(Config.DB_CERT_PATH)
- Contestant.__firebase_init(cred)
- db = firestore.client()
- users_ref = db.collection(Contestant.__FIRE_PATH)
- usrs = users_ref.stream()
- users = []
- for usr in usrs:
- if ((id is None and name is None)
- or (id is not None and usr.id == id)
- or (name is not None and name == usr.to_dict()['fullName'])):
- users.append(Contestant(usr.id, usr.to_dict()))
- return users
diff --git a/PrivateConfig.sample.py b/PrivateConfig.sample.py
new file mode 100644
index 0000000..2644fe9
--- /dev/null
+++ b/PrivateConfig.sample.py
@@ -0,0 +1,2 @@
+SERVICE_TOKEN = 'backend SERVICE TOKEN'
+BASE_URL = 'backend url'
\ No newline at end of file
diff --git a/README.md b/README.md
index d069e03..264a8ec 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,210 @@
-# IdentificationGenerator
-Program used to generate Identification cards
-
-
-to run as a container
-- docker build -t test .
-- docker run -it test /bin/bash
\ No newline at end of file
+# IdentificationGenerator
+
+A FastAPI-based service for generating identification cards for hackathon events. This application creates personalized ID cards with QR codes for contestants, mentors, organizers, volunteers, companies, and guests.
+
+## Features
+
+- 🎫 Generate ID cards for multiple user types
+- 🔲 QR code generation for each participant
+- 📄 PDF export functionality
+- 🔥 Firebase integration for data storage
+- 🚀 FastAPI REST API
+- 🐳 Docker support for easy deployment
+
+## Prerequisites
+
+- Docker and Docker Compose (for containerized deployment)
+- OR Python 3.12+ and uv (for local development)
+
+## Quick Start with Docker
+
+### Production Deployment
+
+1. **Clone the repository**
+ ```bash
+ git clone
+ cd IdentificationGenerator
+ ```
+
+2. **Set up environment variables**
+ ```bash
+ cp .env.example .env
+ # Edit .env with your configuration
+ ```
+
+3. **Build and run with Docker Compose**
+ ```bash
+ docker-compose up -d
+ ```
+
+4. **Access the API**
+ - API: http://localhost:8000
+ - API Documentation: http://localhost:8000/docs
+ - Alternative docs: http://localhost:8000/redoc
+
+### Development Mode
+
+For development with hot-reload:
+
+```bash
+docker-compose -f docker-compose.dev.yml up
+```
+
+This will mount your source code into the container, enabling live reload on code changes.
+
+## Docker Commands
+
+### Build the image
+```bash
+docker build -t identification-generator .
+```
+
+### Run a container
+```bash
+docker run -d -p 8000:8000 --name id-gen identification-generator
+```
+
+### View logs
+```bash
+docker-compose logs -f
+```
+
+### Stop containers
+```bash
+docker-compose down
+```
+
+### Rebuild after changes
+```bash
+docker-compose up -d --build
+```
+
+### Access container shell
+```bash
+docker exec -it identification-generator-api /bin/bash
+```
+
+## Local Development (without Docker)
+
+### Setup
+
+1. **Install uv** (if not already installed)
+ ```bash
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+ ```
+
+2. **Install dependencies**
+ ```bash
+ uv sync
+ ```
+
+3. **Run the application**
+ ```bash
+ uv run uvicorn main:app --reload
+ ```
+
+## Configuration
+
+### Firebase Setup
+
+1. Obtain your Firebase service account credentials
+2. Save the JSON file as `resources/2019_firebase_cert.json`
+3. Update the path in `Config.py` if needed
+
+### Environment Variables
+
+Create a `.env` file based on `.env.example`:
+
+- `EDITION`: The year/edition of the event (default: 2025)
+- `TEST`: Set to `True` for test mode, `False` for production
+
+## API Endpoints
+
+- `GET /` - Health check
+- `GET /docs` - Interactive API documentation
+- `/contestants/*` - Contestant management endpoints
+- `/companies/*` - Company/sponsor endpoints
+- `/guests/*` - Guest management
+- `/mentors/*` - Mentor management
+- `/organizers/*` - Organizer management
+- `/volunteers/*` - Volunteer management
+
+## Project Structure
+
+```
+.
+├── main.py # FastAPI application entry point
+├── Config.py # Configuration settings
+├── dependencies.py # Dependency injection
+├── models/ # Data models
+├── repositories/ # Data access layer
+├── routers/ # API route handlers
+├── services/ # Business logic
+├── resources/ # Static resources (fonts, templates)
+├── tests/ # Test suite
+└── docker-compose.yml # Docker orchestration
+```
+
+## Generated Files
+
+Generated ID cards are stored in the `generated_cards/` directory (mounted as `_out_/` inside the container).
+
+## Testing
+
+Run tests with:
+
+```bash
+# With uv
+uv run pytest
+
+# In Docker
+docker-compose exec api uv run pytest
+```
+
+## Production Deployment
+
+### Using Docker Compose
+
+1. Update `docker-compose.yml` with production settings
+2. Set up proper secrets management for Firebase credentials
+3. Configure nginx reverse proxy (optional, template included)
+4. Set up SSL certificates
+5. Deploy:
+ ```bash
+ docker-compose up -d
+ ```
+
+### Health Checks
+
+The Docker container includes a health check that pings the API every 30 seconds.
+
+## Troubleshooting
+
+### Container won't start
+- Check logs: `docker-compose logs api`
+- Verify all required files are present
+- Ensure Firebase credentials are properly mounted
+
+### Permission issues with generated files
+- Check volume mount permissions
+- Ensure the `generated_cards/` directory exists and is writable
+
+### Missing fonts or resources
+- Verify the `resources/` directory is properly mounted
+- Check that all required fonts are in `resources/fonts/`
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch
+3. Make your changes
+4. Run tests
+5. Submit a pull request
+
+## License
+
+[Add your license here]
+
+## Support
+
+For issues and questions, please open an issue on GitHub.
\ No newline at end of file
diff --git a/Tools.py b/Tools.py
deleted file mode 100644
index 8c2405c..0000000
--- a/Tools.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import hashlib
-import json
-import os
-
-import qrcode
-from PIL import ImageDraw, Image
-import Config
-
-
-def create_dir(path):
- exists = os.path.isdir(path)
- if not exists:
- try:
- os.mkdir(path)
- except OSError:
- raise OSError('dir creation error')
-
-
-def empty_dir(path, delete_files=True, delete_dirs=True):
- exists = os.path.isdir(path)
- if exists and (delete_files or delete_dirs):
- for root, dirs, files in os.walk(path):
- if delete_files:
- for file in files:
- os.remove(os.path.join(root, file))
- if delete_dirs:
- for dir in dirs:
- os.remove(os.path.join(root, dir))
-
-
-def draw_text(image, text, pos, font, fill, centrate=True, mayus=True):
- if mayus:
- text = text.upper()
- draw = ImageDraw.Draw(image)
- w, h = draw.textsize(text, font=font)
- if centrate:
- ImageDraw.Draw(image).text(((image.width-w)/2, pos[1]), text, font=font,fill=fill)
- else:
- ImageDraw.Draw(image).text(pos, text, font=font,fill=fill)
-
-def centrate_text_relative(image, text, font, relative_pos, relative_size, mayus=True):
- #centrate relative on x and split in 2 lines if text width is bigger than relative_size
- if mayus:
- text = text.upper()
- draw = ImageDraw.Draw(image)
- w, h = draw.textsize(text, font=font)
- x = relative_pos[0] + (relative_size[0] - w) / 2
- y = relative_pos[1]
- if w > relative_size[0]:
- #split in 2 lines
- words = text.split(' ')
- line1 = ''
- line2 = ''
- for word in words:
- if draw.textsize(line1 + ' ' + word, font=font)[0] < relative_size[0]:
- line1 += ' ' + word
- else:
- line2 += ' ' + word
- x1 = relative_pos[0] + (relative_size[0] - draw.textsize(line1, font=font)[0]) / 2
- x2 = relative_pos[0] + (relative_size[0] - draw.textsize(line2, font=font)[0]) / 2
- draw.text((x1, y), line1, font=font)
- draw.text((x2, y + h), line2, font=font)
- else:
- ImageDraw.Draw(image).text((x, y), text, font=font)
-
-def scale(image, max_size, add_mask=True, method=Image.ANTIALIAS):
- """
- resize 'image' to 'max_size' keeping the aspect ratio
- and place it in center of white 'max_size' image
- """
- im_aspect = float(image.size[0]) / float(image.size[1])
- out_aspect = float(max_size[0]) / float(max_size[1])
- if im_aspect >= out_aspect:
- scaled = image.resize((max_size[0], int((float(max_size[0]) / im_aspect) + 0.5)), method)
- else:
- scaled = image.resize((int((float(max_size[1]) * im_aspect) + 0.5), max_size[1]), method)
-
- offset = (((max_size[0] - scaled.size[0]) / 2), ((max_size[1] - scaled.size[1]) / 2))
- back = Image.new("RGBA", max_size, Config.BAK_COLOR)
- if add_mask:
- back.paste(scaled, (int(offset[0]), int(offset[1])), scaled)
- else:
- back.paste(scaled, (int(offset[0]), int(offset[1])))
- return back
-
-def generate_qr(input, size, border_size):
- qr = qrcode.QRCode(
- version=2,
- error_correction=qrcode.constants.ERROR_CORRECT_H,
- box_size=size,
- border=border_size)
- qr.add_data(input)
- qr.make(fit=True)
- qr = qr.make_image()
- return qr
-
-
-def crypt(input, method='md5'):
- m = hashlib.new(method)
- m.update(bytes(input, 'utf'))
- return str(m.digest())
-
-
-class DataFile:
- __contents = {}
-
- @staticmethod
- def get_content(filepath, type, reload_if_cached=False):
- if not reload_if_cached and filepath in DataFile.__contents:
- return DataFile.__contents[filepath]
- else:
- with open(filepath, 'r', encoding='utf-8') as json_file:
- if type == 'JSON':
- data = json.load(json_file)
- DataFile.__contents[filepath] = data
- return data
- else:
- raise Exception(NotImplemented)
-
- @staticmethod
- def clear_cached_content(filepath):
- if filepath in DataFile.__contents:
- del DataFile.__contents[filepath]
- return True
- else:
- return False
-
- @staticmethod
- def clear_cache():
- DataFile.__contents = {}
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..578cf15
--- /dev/null
+++ b/config/__init__.py
@@ -0,0 +1,41 @@
+"""
+Configuration package for the Identification Generator.
+
+Provides centralized access to settings, constants, and path management.
+"""
+from config.settings import settings, Settings
+from config.constants import (
+ TYPE_FONT_SIZE,
+ NAME_FONT_SIZE,
+ FONT_COLOR,
+ WHITE_FONT_COLOR,
+ DARK_FONT_COLOR,
+ BROWN_COLOR,
+ MAIN_COLOR,
+ BAK_COLOR,
+ QR_VERSION,
+ QR_BOX_SIZE,
+ QR_BORDER,
+)
+from config.paths import paths, PathManager
+
+__all__ = [
+ # Settings
+ "settings",
+ "Settings",
+ # Constants
+ "TYPE_FONT_SIZE",
+ "NAME_FONT_SIZE",
+ "FONT_COLOR",
+ "WHITE_FONT_COLOR",
+ "DARK_FONT_COLOR",
+ "BROWN_COLOR",
+ "MAIN_COLOR",
+ "BAK_COLOR",
+ "QR_VERSION",
+ "QR_BOX_SIZE",
+ "QR_BORDER",
+ # Paths
+ "paths",
+ "PathManager",
+]
diff --git a/config/constants.py b/config/constants.py
new file mode 100644
index 0000000..097bddb
--- /dev/null
+++ b/config/constants.py
@@ -0,0 +1,22 @@
+"""
+Constants module for truly constant values that don't change across environments.
+Includes colors, font sizes, and other fixed configuration values.
+"""
+from typing import Tuple
+
+# Font Sizes
+TYPE_FONT_SIZE: int = 40
+NAME_FONT_SIZE: int = 60
+
+# Colors (RGB tuples)
+FONT_COLOR: Tuple[int, int, int] = (0, 0, 0)
+WHITE_FONT_COLOR: Tuple[int, int, int] = (84, 49, 26)
+DARK_FONT_COLOR: Tuple[int, int, int] = (35, 35, 35)
+BROWN_COLOR: Tuple[int, int, int] = (101, 67, 33)
+MAIN_COLOR: Tuple[int, int, int] = (31, 33, 36)
+BAK_COLOR: Tuple[int, int, int] = (119, 177, 201)
+
+# QR Code Settings
+QR_VERSION: int = 2
+QR_BOX_SIZE: int = 10
+QR_BORDER: int = 4
diff --git a/config/paths.py b/config/paths.py
new file mode 100644
index 0000000..898608d
--- /dev/null
+++ b/config/paths.py
@@ -0,0 +1,120 @@
+"""
+Path management module for constructing file paths and lazy-loading fonts.
+"""
+from os import path
+from typing import Optional
+from PIL import ImageFont
+
+from config.settings import settings
+from config.constants import TYPE_FONT_SIZE, NAME_FONT_SIZE
+
+
+class PathManager:
+ """Manages path construction and font loading."""
+
+ def __init__(self):
+ self._type_font: Optional[ImageFont.FreeTypeFont] = None
+ self._name_font: Optional[ImageFont.FreeTypeFont] = None
+ self._bold_type_font: Optional[ImageFont.FreeTypeFont] = None
+ self._bold_name_font: Optional[ImageFont.FreeTypeFont] = None
+
+ # Base Paths
+ @property
+ def out_path(self) -> str:
+ """Output folder path."""
+ return path.join('.', settings.OUT_FOLDER)
+
+ @property
+ def res_path(self) -> str:
+ """Resources folder path."""
+ return path.join('.', settings.RES_FOLDER)
+
+ @property
+ def data_path(self) -> str:
+ """Data file path."""
+ return path.join(
+ self.res_path,
+ settings.EDITIONS_FOLDER,
+ settings.EDITION,
+ settings.DATA_FILE
+ )
+
+ @property
+ def db_cert_path(self) -> str:
+ """Database certificate path."""
+ return path.join(self.res_path, settings.DB_CERT)
+
+ # Template Paths
+ @property
+ def bak_path_contestant(self) -> str:
+ """Contestant card template path."""
+ return path.join(
+ self.res_path,
+ settings.EDITIONS_FOLDER,
+ settings.EDITION,
+ settings.BAK_FILE_CONTESTANT
+ )
+
+ @property
+ def bak_path_staff(self) -> str:
+ """Staff card template path."""
+ return path.join(
+ self.res_path,
+ settings.EDITIONS_FOLDER,
+ settings.EDITION,
+ settings.BAK_FILE_STAFF
+ )
+
+ @property
+ def bak_path_empresa(self) -> str:
+ """Company card template path."""
+ return path.join(
+ self.res_path,
+ settings.EDITIONS_FOLDER,
+ settings.EDITION,
+ settings.BAK_FILE_EMPRESA
+ )
+
+ # Font Paths
+ @property
+ def font_path(self) -> str:
+ """Regular font file path."""
+ return path.join(self.res_path, settings.FONT_FOLDER, settings.FONT_FILE)
+
+ @property
+ def bold_font_path(self) -> str:
+ """Bold font file path."""
+ return path.join(self.res_path, settings.FONT_FOLDER, settings.BOLD_FONT_FILE)
+
+ # Lazy-loaded Fonts
+ @property
+ def type_font(self) -> ImageFont.FreeTypeFont:
+ """Regular type font (lazy-loaded)."""
+ if self._type_font is None:
+ self._type_font = ImageFont.truetype(self.font_path, TYPE_FONT_SIZE)
+ return self._type_font
+
+ @property
+ def name_font(self) -> ImageFont.FreeTypeFont:
+ """Regular name font (lazy-loaded)."""
+ if self._name_font is None:
+ self._name_font = ImageFont.truetype(self.font_path, NAME_FONT_SIZE)
+ return self._name_font
+
+ @property
+ def bold_type_font(self) -> ImageFont.FreeTypeFont:
+ """Bold type font (lazy-loaded)."""
+ if self._bold_type_font is None:
+ self._bold_type_font = ImageFont.truetype(self.bold_font_path, TYPE_FONT_SIZE)
+ return self._bold_type_font
+
+ @property
+ def bold_name_font(self) -> ImageFont.FreeTypeFont:
+ """Bold name font (lazy-loaded)."""
+ if self._bold_name_font is None:
+ self._bold_name_font = ImageFont.truetype(self.bold_font_path, NAME_FONT_SIZE)
+ return self._bold_name_font
+
+
+# Singleton instance
+paths = PathManager()
diff --git a/config/settings.py b/config/settings.py
new file mode 100644
index 0000000..32ddbbc
--- /dev/null
+++ b/config/settings.py
@@ -0,0 +1,86 @@
+"""
+Settings module using Pydantic for type-safe configuration management.
+Loads configuration from environment variables with .env file support.
+"""
+from typing import Optional
+from pydantic_settings import BaseSettings, SettingsConfigDict
+from pydantic import Field
+
+
+class Settings(BaseSettings):
+ """Application settings loaded from environment variables."""
+
+ model_config = SettingsConfigDict(
+ env_file=".env",
+ env_file_encoding="utf-8",
+ case_sensitive=True,
+ extra="ignore"
+ )
+
+ # Edition Configuration
+ EDITION: str = Field(default="2025", description="Event edition year")
+ TEST: bool = Field(default=False, description="Test mode flag")
+
+ # Database Paths
+ DB_PATH_T: str = Field(
+ default="hackeps-2025/dev/users",
+ description="Test/development database path"
+ )
+ DB_PATH: str = Field(
+ default="hackeps-2025/prod/users",
+ description="Production database path"
+ )
+
+ # API Configuration
+ BASE_URL: str = Field(
+ default="http://localhost:8000",
+ description="Backend API base URL"
+ )
+ SERVICE_TOKEN: str = Field(
+ default="dummy_token",
+ description="Service authentication token"
+ )
+
+ # Folder Paths (with sensible defaults)
+ EDITIONS_FOLDER: str = Field(default="editions", description="Editions folder name")
+ OUT_FOLDER: str = Field(default="_out_", description="Output folder name")
+ RES_FOLDER: str = Field(default="resources", description="Resources folder name")
+ FONT_FOLDER: str = Field(default="fonts", description="Fonts folder name")
+
+ # File Names
+ DATA_FILE: str = Field(default="data.json", description="Data file name")
+ DB_CERT: str = Field(
+ default="2019_firebase_cert.json",
+ description="Firebase certificate file name"
+ )
+ FONT_FILE: str = Field(
+ default="SpaceMono-Regular.ttf",
+ description="Regular font file name"
+ )
+ BOLD_FONT_FILE: str = Field(
+ default="SpaceMono-Bold.ttf",
+ description="Bold font file name"
+ )
+
+ # Template Files (relative paths within edition folder)
+ BAK_FILE_CONTESTANT: str = Field(
+ default="plantilles/participant.png",
+ description="Contestant card template"
+ )
+ BAK_FILE_STAFF: str = Field(
+ default="plantilles/organitzador.png",
+ description="Staff card template"
+ )
+ BAK_FILE_EMPRESA: str = Field(
+ default="plantilles/patrocinador.png",
+ description="Company card template"
+ )
+
+ @property
+ def db_path(self) -> str:
+ """Get the appropriate database path based on TEST mode."""
+ return self.DB_PATH_T if self.TEST else self.DB_PATH
+
+
+# Singleton instance
+settings = Settings()
diff --git a/dependencies.py b/dependencies.py
new file mode 100644
index 0000000..48079aa
--- /dev/null
+++ b/dependencies.py
@@ -0,0 +1,37 @@
+from services.api_service import APIService
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+from repositories.contestant_repository import ContestantRepository
+from repositories.company_repository import CompanyRepository
+from repositories.guest_repository import GuestRepository
+from repositories.mentor_repository import MentorRepository
+from repositories.organizer_repository import OrganizerRepository
+from repositories.volunteer_repository import VolunteerRepository
+from fastapi import Depends
+
+def get_api_service():
+ return APIService()
+
+def get_pdf_service():
+ return PDFService()
+
+def get_card_generator_service():
+ return CardGeneratorService()
+
+def get_contestant_repository(api_service: APIService = Depends(get_api_service)):
+ return ContestantRepository(api_service)
+
+def get_company_repository(api_service: APIService = Depends(get_api_service)):
+ return CompanyRepository(api_service)
+
+def get_guest_repository():
+ return GuestRepository()
+
+def get_mentor_repository():
+ return MentorRepository()
+
+def get_organizer_repository():
+ return OrganizerRepository()
+
+def get_volunteer_repository():
+ return VolunteerRepository()
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..959d97c
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,30 @@
+version: '3.8'
+
+services:
+ api:
+ build:
+ context: .
+ dockerfile: Dockerfile.dev
+ container_name: identification-generator-dev
+ ports:
+ - "8000:8000"
+ volumes:
+ # Mount source code for hot-reload
+ - .:/app
+ # Exclude virtual environment
+ - /app/.venv
+ # Mount output directory
+ - ./generated_cards:/app/_out_
+ environment:
+ - EDITION=2025
+ - TEST=True
+ restart: unless-stopped
+ networks:
+ - app-network
+ # Enable debugging
+ stdin_open: true
+ tty: true
+
+networks:
+ app-network:
+ driver: bridge
diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example
new file mode 100644
index 0000000..9e50dec
--- /dev/null
+++ b/docker-compose.override.yml.example
@@ -0,0 +1,37 @@
+# Docker Compose Override Example
+#
+# Copy this file to docker-compose.override.yml to customize your local setup
+# docker-compose.override.yml is git-ignored and will automatically be used
+# alongside docker-compose.yml when you run docker-compose commands
+#
+# Example customizations:
+
+version: '3.8'
+
+services:
+ api:
+ # Use a different port
+ # ports:
+ # - "8080:8000"
+
+ # Add additional environment variables
+ # environment:
+ # - DEBUG=True
+ # - LOG_LEVEL=debug
+
+ # Mount additional volumes
+ # volumes:
+ # - ./custom_resources:/app/custom_resources
+
+ # Override the command
+ # command: uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload --log-level debug
+
+ # Add resource limits
+ # deploy:
+ # resources:
+ # limits:
+ # cpus: '2'
+ # memory: 2G
+ # reservations:
+ # cpus: '1'
+ # memory: 512M
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..aeb4a73
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,45 @@
+version: '3.8'
+
+services:
+ api:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: identification-generator-api
+ ports:
+ - "8000:8000"
+ volumes:
+ # Mount output directory for generated files
+ - ./generated_cards:/app/_out_
+ # Mount resources if you need to update them without rebuilding
+ - ./resources:/app/resources:ro
+ # Mount Firebase certificate (create this file from your credentials)
+ # - ./resources/2019_firebase_cert.json:/app/resources/2019_firebase_cert.json:ro
+ environment:
+ # Override these in a .env file or docker-compose.override.yml
+ - EDITION=2025
+ - TEST=False
+ restart: unless-stopped
+ networks:
+ - app-network
+ # Optional: Add nginx reverse proxy for production
+ # nginx:
+ # image: nginx:alpine
+ # container_name: identification-generator-nginx
+ # ports:
+ # - "80:80"
+ # - "443:443"
+ # volumes:
+ # - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ # - ./ssl:/etc/nginx/ssl:ro
+ # depends_on:
+ # - api
+ # networks:
+ # - app-network
+
+networks:
+ app-network:
+ driver: bridge
+
+volumes:
+ generated_cards:
diff --git a/main.py b/main.py
index 23d2916..9203df1 100644
--- a/main.py
+++ b/main.py
@@ -1,26 +1,15 @@
-import Config
-import Model
-import Tools
-from PIL import Image
-import os
-
-users = []
-users += Model.Contestant.get_data()
-users += Model.Organizer.get_data()
-users += Model.Volunteer.get_data()
-users += Model.Mentor.get_data()
-users += Model.Company.get_data()
-users += Model.Guest.get_data()
-users += Model.Assistant.get_data()
-
-Tools.create_dir(Config.OUT_PATH)
-Tools.empty_dir(Config.OUT_PATH)
-
-i = 0
-for u in users:
- u.generate_card()
- u.save()
- i+=1
- # break
-
-print('Generated ' + str(i) + ' cards')
\ No newline at end of file
+from fastapi import FastAPI
+from routers import contestants, companies, guests, mentors, organizers, volunteers
+
+app = FastAPI()
+
+app.include_router(contestants.router)
+app.include_router(companies.router)
+app.include_router(guests.router)
+app.include_router(mentors.router)
+app.include_router(organizers.router)
+app.include_router(volunteers.router)
+
+@app.get("/")
+def read_root():
+ return {"message": "Identification Generator API"}
diff --git a/models/assistant.py b/models/assistant.py
new file mode 100644
index 0000000..dd1c631
--- /dev/null
+++ b/models/assistant.py
@@ -0,0 +1,62 @@
+
+import os
+
+from config.settings import settings
+from config.paths import paths
+from config.constants import BROWN_COLOR
+from models.card import Card
+import tools
+
+from PIL import Image
+
+class Assistant(object):
+ __ID:int = 1
+ __DATA:str = 'empty'
+ _DATA_FILE:str = paths.data_path
+
+ def __init__(self, id:str, type:str='', name:str=None):
+ self.id = id
+ self.type = type
+ self.name = name
+ self.card = None
+ self.qr = None
+ self.code = None
+
+ def show(self):
+ self.card.show()
+
+ def save(self):
+ self.card.save(os.path.join(paths.out_path, str(self.id) + '.png'))
+
+ def generate_qr(self, crypt_id=False):
+ if self.code:
+ self.qr = tools.generate_qr(self.code, Card.QR_PIX_SIZE, Card.QR_BORDER_SIZE)
+ else:
+ self.qr = tools.generate_qr(self.id, Card.QR_PIX_SIZE, Card.QR_BORDER_SIZE)
+ self.qr = tools.scale(self.qr, Card.QR_SIZE, False)
+
+ def generate_card(self, rgb_back=(255, 255, 255), template=None):
+ if template is None:
+ template = paths.bak_path_contestant
+ self.card = Image.open(template)
+ tools.draw_text(self.card, self.type, Card.TYPE_POS, paths.type_font, BROWN_COLOR)
+ if self.type == '':
+ self.smallen()
+
+ def smallen(self):
+ return
+ blank = Image.new('RGB', (1082, 782),(255,255,255))
+ blank.paste(self.card, (0, 0))
+ self.card = blank
+
+ @staticmethod
+ def get_data():
+ data = tools.DataFile.get_content(Assistant._DATA_FILE, 'JSON')
+ num = data[Assistant.__DATA]
+ res = []
+ for _ in range(num):
+ res.append(Assistant('A' + str(Assistant.__ID)))
+ Assistant.__ID += 1
+ if settings.TEST:
+ break
+ return res
diff --git a/models/card.py b/models/card.py
new file mode 100644
index 0000000..eac51f0
--- /dev/null
+++ b/models/card.py
@@ -0,0 +1,9 @@
+class Card:
+ QR_PIX_SIZE = 1
+ QR_POS = (711*1 - 100, 315*2-900//3)
+ _QR_SIZE = 331*1
+ QR_SIZE = (_QR_SIZE, _QR_SIZE)
+ QR_BORDER_SIZE = 2
+ TYPE_POS = (1063*(1/2), 720*(0.8/5))
+ NAME_POS = (1063*(1/2), 720*(0.2/5))
+ NICK_POS = (1063*(2/4), 720*(2/5))
\ No newline at end of file
diff --git a/models/company.py b/models/company.py
new file mode 100644
index 0000000..709ca45
--- /dev/null
+++ b/models/company.py
@@ -0,0 +1,35 @@
+import base64
+import os
+from config.paths import paths
+from config.constants import DARK_FONT_COLOR
+
+import requests
+from io import BytesIO
+import tools
+from models.assistant import Assistant
+
+from PIL import Image
+
+from models.card import Card
+
+class Company(Assistant):
+ __ID:int = 1
+ __TYPE:str = 'Empresa'
+ __DATA:str = 'companies'
+
+ def __init__(self, name:str, image):
+ super().__init__('C' + str(Company.__ID), Company.__TYPE, name)
+ Company.__ID += 1
+ self.logo = image
+
+ def generate_card(self, rgb_back=(255, 255, 255)):
+ print("Generating card for company:", self.name)
+ self.card = Image.open(paths.bak_path_empresa)
+ tools.draw_text(self.card, self.type, Card.TYPE_POS, paths.type_font, DARK_FONT_COLOR)
+ if self.type == '':
+ self.smallen()
+ image = tools.translate_image(self.logo)
+ image = tools.scale(image, Card.QR_SIZE)
+ self.card.paste(image, Card.QR_POS)
+ tools.centrate_text_relative(self.card, self.name.strip(), paths.bold_name_font, Card.NAME_POS, Card.QR_SIZE * 3, DARK_FONT_COLOR)
+ self.smallen()
diff --git a/models/contestant.py b/models/contestant.py
new file mode 100644
index 0000000..2dd22a9
--- /dev/null
+++ b/models/contestant.py
@@ -0,0 +1,35 @@
+from config.settings import settings
+from config.paths import paths
+from config.constants import WHITE_FONT_COLOR
+import tools
+
+from models.assistant import Assistant
+from models.card import Card
+
+
+class Contestant(Assistant):
+ __CRYPT_ID = False
+ __TYPE:str = 'Participant'
+ __FIREBASE = None
+ __FIRE_PATH:str = settings.db_path
+
+ def __init__(self, id, data):
+ super().__init__(id, Contestant.__TYPE)
+ self.code = data['code']
+ self.generate_qr()
+ self.name = data['name']
+ self.nick = '\"' + data['nickname'] + '\"'
+
+ def generate_card(self, rgb_back=(255, 255, 255)):
+ super().generate_card(rgb_back, paths.bak_path_contestant)
+ self.card.paste(self.qr, Card.QR_POS)
+ tools.centrate_text_relative(self.card, " ".join(self.name.split(" ")[:2]).strip(), paths.bold_name_font, Card.NAME_POS, Card.QR_SIZE * 3, WHITE_FONT_COLOR)
+ self.smallen()
+
+ # @staticmethod
+ # def __firebase_init(cred):
+ # if Contestant.__FIREBASE is None:
+ # Contestant.__FIREBASE = firebase_admin.initialize_app(cred)
+ # return Contestant.__FIREBASE
+
+
diff --git a/models/guest.py b/models/guest.py
new file mode 100644
index 0000000..9aed8af
--- /dev/null
+++ b/models/guest.py
@@ -0,0 +1,49 @@
+import os
+from config.settings import settings
+from config.paths import paths
+from config.constants import DARK_FONT_COLOR
+from models.card import Card
+import tools
+from models.assistant import Assistant
+from PIL import Image
+
+class Guest(Assistant):
+ __ID:int = 1
+ __TYPE:str = 'Convidat'
+ __DATA:str = 'guests'
+
+ def __init__(self, name:str, mtype:str='', logo:str='', has_qr:bool=False):
+ super().__init__('HackEPS_Guest_' + str(Guest.__ID), (mtype, Guest.__TYPE)[mtype == ''], name)
+ Guest.__ID += 1
+ self.has_qr = has_qr
+ self.logo = logo
+ if has_qr:
+ self.generate_qr(True)
+ if not logo == '':
+ self.logopath = os.path.join(paths.res_path, settings.EDITIONS_FOLDER, settings.EDITION, 'images', logo)
+
+ def generate_card(self, rgb_back=(255, 255, 255)):
+ self.card = Image.open(paths.bak_path_staff)
+ tools.draw_text(self.card, self.type, Card.TYPE_POS, paths.type_font, DARK_FONT_COLOR)
+ if self.type == '':
+ self.smallen()
+ if self.logo != '':
+ logo = Image.open(self.logopath).convert("RGBA")
+ logo = tools.scale(logo, Card.QR_SIZE)
+ self.card.paste(logo, Card.QR_POS)
+ elif self.has_qr:
+ self.card.paste(self.qr, Card.QR_POS)
+ if self.name != '':
+ tools.centrate_text_relative(self.card, " ".join(self.name.split(" ")[:2]).strip(), paths.bold_name_font, Card.NAME_POS, Card.QR_SIZE * 3, DARK_FONT_COLOR)
+ self.smallen()
+
+ @staticmethod
+ def get_data(name=None):
+ res = []
+ data = tools.DataFile.get_content(Guest._DATA_FILE, 'JSON')
+ for u in data[Guest.__DATA]:
+ if name is None or u['name'] == name:
+ res.append(Guest(u['name'], u['type'], u['logo'], u['qr']))
+ if settings.TEST or (name is not None and u['name'] == name):
+ break
+ return res
\ No newline at end of file
diff --git a/models/mentor.py b/models/mentor.py
new file mode 100644
index 0000000..e9d5e96
--- /dev/null
+++ b/models/mentor.py
@@ -0,0 +1,42 @@
+import os
+from config.settings import settings
+from config.paths import paths
+from config.constants import DARK_FONT_COLOR
+import tools
+from models.assistant import Assistant
+from models.card import Card
+
+from PIL import Image
+
+
+class Mentor(Assistant):
+ __ID:int = 1
+ __LOGO_PATH:str = os.path.join(paths.res_path, 'editions', settings.EDITION, 'images', 'logogran.png')
+ __TYPE:str = 'Mentor/a'
+ __DATA:str = 'mentors'
+
+ def __init__(self, name:str):
+ super().__init__('M' + str(Mentor.__ID), Mentor.__TYPE, name)
+ Mentor.__ID += 1
+
+ def generate_card(self, rgb_back=(255, 255, 255)):
+ self.card = Image.open(paths.bak_path_staff)
+ tools.draw_text(self.card, self.type, Card.TYPE_POS, paths.type_font, DARK_FONT_COLOR)
+ if self.type == '':
+ self.smallen()
+ image = Image.open(Mentor.__LOGO_PATH).convert("RGBA")
+ image = tools.scale(image, Card.QR_SIZE)
+ self.card.paste(image, Card.QR_POS)
+ tools.centrate_text_relative(self.card, " ".join(self.name.split(" ")[:2]).strip(), paths.bold_name_font, Card.NAME_POS, Card.QR_SIZE * 3, DARK_FONT_COLOR)
+ self.smallen()
+
+ @staticmethod
+ def get_data(name=None):
+ res = []
+ data = tools.DataFile.get_content(Mentor._DATA_FILE, 'JSON')
+ for u in data[Mentor.__DATA]:
+ if name is None or name == u['name']:
+ res.append(Mentor(u['name']))
+ if settings.TEST or (name is not None and name == u['name']):
+ break
+ return res
\ No newline at end of file
diff --git a/models/organizer.py b/models/organizer.py
new file mode 100644
index 0000000..435e8d5
--- /dev/null
+++ b/models/organizer.py
@@ -0,0 +1,42 @@
+import os
+from PIL import Image
+from config.settings import settings
+from config.paths import paths
+from config.constants import DARK_FONT_COLOR
+import tools
+from models.assistant import Assistant
+from models.card import Card
+
+
+class Organizer(Assistant):
+ __ID:int = 1
+ __LOGO_PATH:str = os.path.join(paths.res_path, 'editions', settings.EDITION, 'images', 'logogran.png')
+ __TYPE:str = 'Staff'
+ __DATA:str = 'organizers'
+
+ def __init__(self, name):
+ super().__init__('O' + str(Organizer.__ID), Organizer.__TYPE, name)
+ Organizer.__ID += 1
+
+ def generate_card(self, rgb_back=(255, 255, 255)):
+ self.card = Image.open(paths.bak_path_staff)
+ tools.draw_text(self.card, self.type, Card.TYPE_POS, paths.type_font, DARK_FONT_COLOR)
+ if self.type == '':
+ self.smallen()
+ image = Image.open(Organizer.__LOGO_PATH).convert("RGBA")
+ image = tools.scale(image, Card.QR_SIZE)
+ self.card.paste(image, Card.QR_POS)
+ tools.centrate_text_relative(self.card, " ".join(self.name.split(" ")[:2]).strip(), paths.bold_name_font, Card.NAME_POS, Card.QR_SIZE * 3, DARK_FONT_COLOR)
+ # Tools.draw_text(self.card, self.name, Card.NAME_POS, Config.NAME_FONT, Config.WHITE_FONT_COLOR, False)
+ self.smallen()
+
+ @staticmethod
+ def get_data(name=None):
+ res = []
+ data = tools.DataFile.get_content(Organizer._DATA_FILE, 'JSON')
+ for u in data[Organizer.__DATA]:
+ if name is None or u['name'] == name:
+ res.append(Organizer(u['name']))
+ if settings.TEST or (name is not None and u['name'] == name):
+ break
+ return res
\ No newline at end of file
diff --git a/models/volunteer.py b/models/volunteer.py
new file mode 100644
index 0000000..84e2b3d
--- /dev/null
+++ b/models/volunteer.py
@@ -0,0 +1,41 @@
+import os
+
+from config.settings import settings
+from config.paths import paths
+from config.constants import DARK_FONT_COLOR
+import tools
+from models.assistant import Assistant
+from models.card import Card
+from PIL import Image
+
+class Volunteer(Assistant):
+ __ID:int = 1
+ __LOGO_PATH:str = os.path.join(paths.res_path, 'editions', settings.EDITION, 'images', 'logogran.png')
+ __TYPE:str = 'Voluntari/a'
+ __DATA:str = 'volunteers'
+
+ def __init__(self, name:str):
+ super().__init__('V' + str(Volunteer.__ID), Volunteer.__TYPE, name)
+ Volunteer.__ID += 1
+
+ def generate_card(self, rgb_back=(255, 255, 255)):
+ self.card = Image.open(paths.bak_path_staff)
+ tools.draw_text(self.card, self.type, Card.TYPE_POS, paths.type_font, DARK_FONT_COLOR)
+ if self.type == '':
+ self.smallen()
+ image = Image.open(Volunteer.__LOGO_PATH).convert("RGBA")
+ image = tools.scale(image, Card.QR_SIZE)
+ self.card.paste(image, Card.QR_POS)
+ tools.centrate_text_relative(self.card, " ".join(self.name.split(" ")[:2]).strip(), paths.bold_name_font, Card.NAME_POS, Card.QR_SIZE * 3, DARK_FONT_COLOR)
+ self.smallen()
+
+ @staticmethod
+ def get_data(name=None):
+ res = []
+ data = tools.DataFile.get_content(Volunteer._DATA_FILE, 'JSON')
+ for u in data[Volunteer.__DATA]:
+ if name is None or name == u['name']:
+ res.append(Volunteer(u['name']))
+ if settings.TEST or (name is not None and name == u['name']):
+ break
+ return res
diff --git a/old.requirements.txt b/old.requirements.txt
deleted file mode 100644
index 57004d6..0000000
--- a/old.requirements.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-CacheControl==0.12.5
-cachetools==3.1.1
-certifi==2019.9.11
-chardet==3.0.4
-firebase-admin==3.2.0
-google-api-core==1.14.3
-google-api-python-client==1.7.11
-google-auth==1.7.1
-google-auth-httplib2==0.0.3
-google-cloud-core==1.0.3
-google-cloud-firestore==1.6.0
-google-cloud-storage==1.23.0
-google-resumable-media==0.5.0
-googleapis-common-protos==1.6.0
-# grpcio==1.25.0
-grpcio==1.50.0
-httplib2==0.19.0
-idna==2.8
-msgpack==0.6.2
-Pillow==8.3.2
-# pkg-resources==0.0.0
-# protobuf==3.15.0
-protobuf==4.21.6
-pyasn1==0.4.7
-pyasn1-modules==0.2.7
-pytz==2019.3
-qrcode==6.1
-requests==2.22.0
-rsa==4.0
-six==1.13.0
-uritemplate==3.0.0
-urllib3==1.22.0
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..619cc70
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,33 @@
+[project]
+name = "identificationgenerator"
+version = "0.1.0"
+description = ""
+readme = "README.md"
+authors = [
+ { name = "Ton", email = "tonlls1999@gmail.com" }
+]
+requires-python = ">=3.12"
+dependencies = [
+ "pillow>=11.0.0",
+ "qrcode>=8.0",
+ "requests>=2.32.3",
+ "isort>=5.13.2",
+ "fpdf>=1.7.2",
+ "reportlab>=4.4.5",
+ "fastapi>=0.123.10",
+ "uvicorn>=0.38.0",
+ "pydantic-settings>=2.0.0",
+ "python-dotenv>=1.0.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.uv]
+package = false
+
+[dependency-groups]
+dev = [
+ "pytest>=9.0.1",
+]
diff --git a/repositories/company_repository.py b/repositories/company_repository.py
new file mode 100644
index 0000000..59f0b0b
--- /dev/null
+++ b/repositories/company_repository.py
@@ -0,0 +1,24 @@
+from typing import List
+
+from services.api_service import APIService
+from config.settings import settings
+from models.company import Company
+
+class CompanyRepository:
+ def __init__(self, api_service: APIService):
+ self.api_service = api_service
+
+ def get_all(self) -> List[Company]:
+ res = []
+ tiers_config = [(1, 8), (2, 5), (3, 5)]
+
+ for tier_num, count in tiers_config:
+ comps = self.api_service.get_companies_by_tier(tier_num)
+ for comp in comps:
+ for _ in range(count):
+ res.append(Company(comp['name'], comp['image']))
+ if settings.TEST:
+ break
+ if settings.TEST:
+ break
+ return res
diff --git a/repositories/contestant_repository.py b/repositories/contestant_repository.py
new file mode 100644
index 0000000..506efb7
--- /dev/null
+++ b/repositories/contestant_repository.py
@@ -0,0 +1,18 @@
+from typing import List, Optional
+from models.contestant import Contestant
+from services.api_service import APIService
+
+class ContestantRepository:
+ def __init__(self, api_service: APIService):
+ self.api_service = api_service
+
+ def get_all(self) -> List[Contestant]:
+ usrs = self.api_service.get_accepted_hackers()
+ return [Contestant(usr['id'], usr) for usr in usrs]
+
+ def get_by_id(self, user_id: str) -> Optional[Contestant]:
+ usrs = self.api_service.get_accepted_hackers()
+ for usr in usrs:
+ if str(usr['id']) == str(user_id):
+ return Contestant(usr['id'], usr)
+ return None
diff --git a/repositories/guest_repository.py b/repositories/guest_repository.py
new file mode 100644
index 0000000..548c5e7
--- /dev/null
+++ b/repositories/guest_repository.py
@@ -0,0 +1,21 @@
+from typing import List, Optional
+from models.guest import Guest
+import tools
+
+class GuestRepository:
+ def __init__(self):
+ pass
+
+ def get_all(self) -> List[Guest]:
+ res = []
+ data = tools.DataFile.get_content(Guest._DATA_FILE, 'JSON')
+ for u in data['guests']:
+ res.append(Guest(u['name'], u['type'], u['logo'], u['qr']))
+ return res
+
+ def get_by_name(self, name: str) -> Optional[Guest]:
+ data = tools.DataFile.get_content(Guest._DATA_FILE, 'JSON')
+ for u in data['guests']:
+ if u['name'] == name:
+ return Guest(u['name'], u['type'], u['logo'], u['qr'])
+ return None
diff --git a/repositories/mentor_repository.py b/repositories/mentor_repository.py
new file mode 100644
index 0000000..a031251
--- /dev/null
+++ b/repositories/mentor_repository.py
@@ -0,0 +1,21 @@
+from typing import List, Optional
+from models.mentor import Mentor
+import tools
+
+class MentorRepository:
+ def __init__(self):
+ pass
+
+ def get_all(self) -> List[Mentor]:
+ res = []
+ data = tools.DataFile.get_content(Mentor._DATA_FILE, 'JSON')
+ for u in data['mentors']:
+ res.append(Mentor(u['name']))
+ return res
+
+ def get_by_name(self, name: str) -> Optional[Mentor]:
+ data = tools.DataFile.get_content(Mentor._DATA_FILE, 'JSON')
+ for u in data['mentors']:
+ if u['name'] == name:
+ return Mentor(u['name'])
+ return None
diff --git a/repositories/organizer_repository.py b/repositories/organizer_repository.py
new file mode 100644
index 0000000..42a9fb6
--- /dev/null
+++ b/repositories/organizer_repository.py
@@ -0,0 +1,21 @@
+from typing import List, Optional
+from models.organizer import Organizer
+import tools
+
+class OrganizerRepository:
+ def __init__(self):
+ pass
+
+ def get_all(self) -> List[Organizer]:
+ res = []
+ data = tools.DataFile.get_content(Organizer._DATA_FILE, 'JSON')
+ for u in data['organizers']:
+ res.append(Organizer(u['name']))
+ return res
+
+ def get_by_name(self, name: str) -> Optional[Organizer]:
+ data = tools.DataFile.get_content(Organizer._DATA_FILE, 'JSON')
+ for u in data['organizers']:
+ if u['name'] == name:
+ return Organizer(u['name'])
+ return None
diff --git a/repositories/volunteer_repository.py b/repositories/volunteer_repository.py
new file mode 100644
index 0000000..d7ab66b
--- /dev/null
+++ b/repositories/volunteer_repository.py
@@ -0,0 +1,21 @@
+from typing import List, Optional
+from models.volunteer import Volunteer
+import tools
+
+class VolunteerRepository:
+ def __init__(self):
+ pass
+
+ def get_all(self) -> List[Volunteer]:
+ res = []
+ data = tools.DataFile.get_content(Volunteer._DATA_FILE, 'JSON')
+ for u in data['volunteers']:
+ res.append(Volunteer(u['name']))
+ return res
+
+ def get_by_name(self, name: str) -> Optional[Volunteer]:
+ data = tools.DataFile.get_content(Volunteer._DATA_FILE, 'JSON')
+ for u in data['volunteers']:
+ if u['name'] == name:
+ return Volunteer(u['name'])
+ return None
diff --git a/requirements.txt b/requirements.txt
index 3ce8d51..59ba41e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,11 +13,11 @@ google-cloud-storage==2.8.0
google-resumable-media==2.4.1
googleapis-common-protos==1.59.0
# grpcio==1.25.0
-grpcio==1.53.0
+grpcio==1.76.0
httplib2==0.22.0
idna==3.4
msgpack==1.0.5
-Pillow==9.5.0
+Pillow==12.0.0
# pkg-resources==0.0.0
# protobuf==3.15.0
protobuf==4.22.1
@@ -29,4 +29,5 @@ requests==2.28.2
rsa==4.9
six==1.16.0
uritemplate==3.0.0
-urllib3==1.26.15
\ No newline at end of file
+urllib3==1.26.15
+cairosvg>=2.0.0
\ No newline at end of file
diff --git a/resources/editions/2023/plantilles/organitzador.png b/resources/editions/2023/plantilles/organitzador.png
new file mode 100644
index 0000000..87b4a00
Binary files /dev/null and b/resources/editions/2023/plantilles/organitzador.png differ
diff --git a/resources/editions/2023/plantilles/participant.png b/resources/editions/2023/plantilles/participant.png
new file mode 100644
index 0000000..ad0f5bc
Binary files /dev/null and b/resources/editions/2023/plantilles/participant.png differ
diff --git a/resources/editions/2023/plantilles/patrocinador.png b/resources/editions/2023/plantilles/patrocinador.png
new file mode 100644
index 0000000..edf6d04
Binary files /dev/null and b/resources/editions/2023/plantilles/patrocinador.png differ
diff --git a/resources/editions/2024/images/logogran.png b/resources/editions/2024/images/logogran.png
new file mode 100644
index 0000000..7d8aa36
Binary files /dev/null and b/resources/editions/2024/images/logogran.png differ
diff --git a/resources/editions/2024/images/paeria.png b/resources/editions/2024/images/paeria.png
new file mode 100644
index 0000000..90c91ed
Binary files /dev/null and b/resources/editions/2024/images/paeria.png differ
diff --git a/resources/editions/2024/images/udl.png b/resources/editions/2024/images/udl.png
new file mode 100644
index 0000000..aeb6971
Binary files /dev/null and b/resources/editions/2024/images/udl.png differ
diff --git a/resources/editions/2024/plantilles/organitzador.png b/resources/editions/2024/plantilles/organitzador.png
new file mode 100644
index 0000000..ac4388d
Binary files /dev/null and b/resources/editions/2024/plantilles/organitzador.png differ
diff --git a/resources/editions/2024/plantilles/participant.png b/resources/editions/2024/plantilles/participant.png
new file mode 100644
index 0000000..3d1fd88
Binary files /dev/null and b/resources/editions/2024/plantilles/participant.png differ
diff --git a/resources/editions/2024/plantilles/patrocinador.png b/resources/editions/2024/plantilles/patrocinador.png
new file mode 100644
index 0000000..8b73c2c
Binary files /dev/null and b/resources/editions/2024/plantilles/patrocinador.png differ
diff --git a/resources/editions/2025/images/agrotecnio.png b/resources/editions/2025/images/agrotecnio.png
new file mode 100644
index 0000000..e2be4a3
Binary files /dev/null and b/resources/editions/2025/images/agrotecnio.png differ
diff --git a/resources/editions/2025/images/bonarea.png b/resources/editions/2025/images/bonarea.png
new file mode 100644
index 0000000..adb7996
Binary files /dev/null and b/resources/editions/2025/images/bonarea.png differ
diff --git a/resources/editions/2025/images/eurecat.png b/resources/editions/2025/images/eurecat.png
new file mode 100644
index 0000000..846a6c5
Binary files /dev/null and b/resources/editions/2025/images/eurecat.png differ
diff --git a/resources/editions/2025/images/ingroup.png b/resources/editions/2025/images/ingroup.png
new file mode 100644
index 0000000..750da9a
Binary files /dev/null and b/resources/editions/2025/images/ingroup.png differ
diff --git a/resources/editions/2025/images/leitat.png b/resources/editions/2025/images/leitat.png
new file mode 100644
index 0000000..ea5723d
Binary files /dev/null and b/resources/editions/2025/images/leitat.png differ
diff --git a/resources/editions/2025/images/logogran.png b/resources/editions/2025/images/logogran.png
new file mode 100644
index 0000000..7d8aa36
Binary files /dev/null and b/resources/editions/2025/images/logogran.png differ
diff --git a/resources/editions/2025/images/paeria.png b/resources/editions/2025/images/paeria.png
new file mode 100644
index 0000000..90c91ed
Binary files /dev/null and b/resources/editions/2025/images/paeria.png differ
diff --git a/resources/editions/2025/images/restbai.png b/resources/editions/2025/images/restbai.png
new file mode 100644
index 0000000..cf2ffe1
Binary files /dev/null and b/resources/editions/2025/images/restbai.png differ
diff --git a/resources/editions/2025/images/udl.png b/resources/editions/2025/images/udl.png
new file mode 100644
index 0000000..aeb6971
Binary files /dev/null and b/resources/editions/2025/images/udl.png differ
diff --git a/resources/editions/2025/plantilles/mentor.png b/resources/editions/2025/plantilles/mentor.png
new file mode 100644
index 0000000..23b8721
Binary files /dev/null and b/resources/editions/2025/plantilles/mentor.png differ
diff --git a/resources/editions/2025/plantilles/organitzador.png b/resources/editions/2025/plantilles/organitzador.png
new file mode 100644
index 0000000..1c464cd
Binary files /dev/null and b/resources/editions/2025/plantilles/organitzador.png differ
diff --git a/resources/editions/2025/plantilles/participant.png b/resources/editions/2025/plantilles/participant.png
new file mode 100644
index 0000000..7c75300
Binary files /dev/null and b/resources/editions/2025/plantilles/participant.png differ
diff --git a/resources/editions/2025/plantilles/patrocinador.png b/resources/editions/2025/plantilles/patrocinador.png
new file mode 100644
index 0000000..56f0d63
Binary files /dev/null and b/resources/editions/2025/plantilles/patrocinador.png differ
diff --git a/resources/editions/data_sample.json b/resources/editions/data_sample.json
index 411efb8..632f4c1 100644
--- a/resources/editions/data_sample.json
+++ b/resources/editions/data_sample.json
@@ -1,15 +1,53 @@
-{
- "empty": 5,
- "organizers": [
- {"name":"Ton Llucià Senserrich"}
- ],
- "volunteers": [
- {"name":"Ton Llucià Senserrich"}
- ],
- "companies": [
- {"name":"Ton Llucià Senserrich","number_of_cards":5,"logo":"logo.png"}
- ],
- "guests": [
- {"name":"Ton Llucià Senserrich","type":"HACKER","qr":true}
- ]
-}
+{
+ "empty": 5,
+ "organizers": [
+ {"name":"Pol Llinàs"},
+ {"name":"Naïm Saadi"},
+ {"name":"Lluc Vivet"},
+ {"name":"Lotfi Bouakel (LOLO)"},
+ {"name":"Ton Llucià "},
+ {"name":"Joel Ros "},
+ {"name":"Gerard Tersa "},
+ {"name":"Alba Serrano "},
+ {"name":"Aida Chavero "},
+ {"name":"Martina López "},
+ {"name":"Anaïs Garrofé "},
+ {"name":"Júlia Calvo "},
+ {"name":"Aleix Rosinach "},
+ {"name":"Andreu Puig-gròs "},
+ {"name":"Enric Zhang "},
+ {"name":"David Garcia "},
+ {"name":"Nataly Jaya "},
+ {"name":"Toni López "},
+ {"name":"Òscar van de Crommert"},
+ {"name":"Carles Sànchez"},
+ {"name":"Gerard Ballebrera"},
+ {"name":"Albert Sorribes"},
+ {"name":"Bru Pallàs "},
+ {"name":"Norbert Aguilera"},
+ {"name":"Aleix Bertran"}
+
+ ],
+ "volunteers": [
+ {"name":"Ton Llucià Senserrich"}
+ ],
+ "companies": [
+ {"name":"Ton Llucià Senserrich","number_of_cards":5,"logo":"logo.png"}
+ ],
+ "guests": [
+ {"name":"Ton Llucià Senserrich","type":"HACKER","qr":true}
+ ],
+ "mentors": [
+ {"name":"David Sarrat","qr":true},
+ {"name":"Sergi Vila","qr":true},
+ {"name":"Alba Lamas","qr":true},
+ {"name":"Josep","qr":true},
+ {"name":"Jordi Onrubia","qr":true},
+ {"name":"Cangrejo #1"},
+ {"name":"Cangrejo #2"},
+ {"name":"Cangrejo #3"},
+ {"name":"ivansaureu15"},
+ {"name":"Josep Maria Salvia"},
+ {"name":"Salce"}
+ ]
+}
diff --git a/routers/companies.py b/routers/companies.py
new file mode 100644
index 0000000..5a56c04
--- /dev/null
+++ b/routers/companies.py
@@ -0,0 +1,30 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import List, Dict, Any
+from repositories.company_repository import CompanyRepository
+from dependencies import get_company_repository, get_pdf_service, get_card_generator_service
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+
+router = APIRouter(prefix="/companies", tags=["companies"])
+
+@router.get("")
+def get_companies(repo: CompanyRepository = Depends(get_company_repository)) -> List[Dict[str, Any]]:
+ companies = repo.get_all()
+ return [{"name": c.name, "id": c.id, "type": c.type} for c in companies]
+
+@router.get("/pdf")
+def get_companies_pdf(
+ repo: CompanyRepository = Depends(get_company_repository),
+ pdf_service: PDFService = Depends(get_pdf_service),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ companies = repo.get_all()
+ if not companies:
+ raise HTTPException(status_code=404, detail="No companies found")
+
+ try:
+ images = card_service.generate_multiple_cards(companies)
+ pdf_bytes = pdf_service.create_pdf(images)
+ return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=companies.pdf"})
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/routers/contestants.py b/routers/contestants.py
new file mode 100644
index 0000000..a57b1c9
--- /dev/null
+++ b/routers/contestants.py
@@ -0,0 +1,46 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import List, Dict, Any
+from repositories.contestant_repository import ContestantRepository
+from dependencies import get_contestant_repository, get_pdf_service, get_card_generator_service
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+
+router = APIRouter(prefix="/contestants", tags=["contestants"])
+
+@router.get("")
+def get_contestants(repo: ContestantRepository = Depends(get_contestant_repository)) -> List[Dict[str, Any]]:
+ users = repo.get_all()
+ return [{"name": u.name, "id": u.id, "type": u.type} for u in users]
+
+@router.get("/pdf")
+def get_contestants_pdf(
+ repo: ContestantRepository = Depends(get_contestant_repository),
+ pdf_service: PDFService = Depends(get_pdf_service),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ users = repo.get_all()
+ if not users:
+ raise HTTPException(status_code=404, detail="No contestants found")
+
+ try:
+ images = card_service.generate_multiple_cards(users)
+ pdf_bytes = pdf_service.create_pdf(images)
+ return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=contestants.pdf"})
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/{user_id}/card")
+def get_contestant_card(
+ user_id: str,
+ repo: ContestantRepository = Depends(get_contestant_repository),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ user = repo.get_by_id(user_id)
+ if not user:
+ raise HTTPException(status_code=404, detail="Contestant not found")
+
+ try:
+ img_bytes = card_service.generate_card_png_bytes(user)
+ return Response(content=img_bytes, media_type="image/png")
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/routers/guests.py b/routers/guests.py
new file mode 100644
index 0000000..b5333aa
--- /dev/null
+++ b/routers/guests.py
@@ -0,0 +1,46 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import List, Dict, Any
+from repositories.guest_repository import GuestRepository
+from dependencies import get_guest_repository, get_pdf_service, get_card_generator_service
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+
+router = APIRouter(prefix="/guests", tags=["guests"])
+
+@router.get("")
+def get_guests(repo: GuestRepository = Depends(get_guest_repository)) -> List[Dict[str, Any]]:
+ guests = repo.get_all()
+ return [{"name": g.name, "id": g.id, "type": g.type} for g in guests]
+
+@router.get("/pdf")
+def get_guests_pdf(
+ repo: GuestRepository = Depends(get_guest_repository),
+ pdf_service: PDFService = Depends(get_pdf_service),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ guests = repo.get_all()
+ if not guests:
+ raise HTTPException(status_code=404, detail="No guests found")
+
+ try:
+ images = card_service.generate_multiple_cards(guests)
+ pdf_bytes = pdf_service.create_pdf(images)
+ return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=guests.pdf"})
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/{name}/card")
+def get_guest_card(
+ name: str,
+ repo: GuestRepository = Depends(get_guest_repository),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ guest = repo.get_by_name(name)
+ if not guest:
+ raise HTTPException(status_code=404, detail="Guest not found")
+
+ try:
+ img_bytes = card_service.generate_card_png_bytes(guest)
+ return Response(content=img_bytes, media_type="image/png")
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/routers/mentors.py b/routers/mentors.py
new file mode 100644
index 0000000..c1b1beb
--- /dev/null
+++ b/routers/mentors.py
@@ -0,0 +1,46 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import List, Dict, Any
+from repositories.mentor_repository import MentorRepository
+from dependencies import get_mentor_repository, get_pdf_service, get_card_generator_service
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+
+router = APIRouter(prefix="/mentors", tags=["mentors"])
+
+@router.get("")
+def get_mentors(repo: MentorRepository = Depends(get_mentor_repository)) -> List[Dict[str, Any]]:
+ mentors = repo.get_all()
+ return [{"name": m.name, "id": m.id, "type": m.type} for m in mentors]
+
+@router.get("/pdf")
+def get_mentors_pdf(
+ repo: MentorRepository = Depends(get_mentor_repository),
+ pdf_service: PDFService = Depends(get_pdf_service),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ mentors = repo.get_all()
+ if not mentors:
+ raise HTTPException(status_code=404, detail="No mentors found")
+
+ try:
+ images = card_service.generate_multiple_cards(mentors)
+ pdf_bytes = pdf_service.create_pdf(images)
+ return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=mentors.pdf"})
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/{name}/card")
+def get_mentor_card(
+ name: str,
+ repo: MentorRepository = Depends(get_mentor_repository),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ mentor = repo.get_by_name(name)
+ if not mentor:
+ raise HTTPException(status_code=404, detail="Mentor not found")
+
+ try:
+ img_bytes = card_service.generate_card_png_bytes(mentor)
+ return Response(content=img_bytes, media_type="image/png")
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/routers/organizers.py b/routers/organizers.py
new file mode 100644
index 0000000..e6745b0
--- /dev/null
+++ b/routers/organizers.py
@@ -0,0 +1,46 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import List, Dict, Any
+from repositories.organizer_repository import OrganizerRepository
+from dependencies import get_organizer_repository, get_pdf_service, get_card_generator_service
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+
+router = APIRouter(prefix="/organizers", tags=["organizers"])
+
+@router.get("")
+def get_organizers(repo: OrganizerRepository = Depends(get_organizer_repository)) -> List[Dict[str, Any]]:
+ organizers = repo.get_all()
+ return [{"name": o.name, "id": o.id, "type": o.type} for o in organizers]
+
+@router.get("/pdf")
+def get_organizers_pdf(
+ repo: OrganizerRepository = Depends(get_organizer_repository),
+ pdf_service: PDFService = Depends(get_pdf_service),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ organizers = repo.get_all()
+ if not organizers:
+ raise HTTPException(status_code=404, detail="No organizers found")
+
+ try:
+ images = card_service.generate_multiple_cards(organizers)
+ pdf_bytes = pdf_service.create_pdf(images)
+ return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=organizers.pdf"})
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/{name}/card")
+def get_organizer_card(
+ name: str,
+ repo: OrganizerRepository = Depends(get_organizer_repository),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ organizer = repo.get_by_name(name)
+ if not organizer:
+ raise HTTPException(status_code=404, detail="Organizer not found")
+
+ try:
+ img_bytes = card_service.generate_card_png_bytes(organizer)
+ return Response(content=img_bytes, media_type="image/png")
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/routers/volunteers.py b/routers/volunteers.py
new file mode 100644
index 0000000..e038950
--- /dev/null
+++ b/routers/volunteers.py
@@ -0,0 +1,46 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import List, Dict, Any
+from repositories.volunteer_repository import VolunteerRepository
+from dependencies import get_volunteer_repository, get_pdf_service, get_card_generator_service
+from services.pdf_service import PDFService
+from services.card_generator_service import CardGeneratorService
+
+router = APIRouter(prefix="/volunteers", tags=["volunteers"])
+
+@router.get("")
+def get_volunteers(repo: VolunteerRepository = Depends(get_volunteer_repository)) -> List[Dict[str, Any]]:
+ volunteers = repo.get_all()
+ return [{"name": v.name, "id": v.id, "type": v.type} for v in volunteers]
+
+@router.get("/pdf")
+def get_volunteers_pdf(
+ repo: VolunteerRepository = Depends(get_volunteer_repository),
+ pdf_service: PDFService = Depends(get_pdf_service),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ volunteers = repo.get_all()
+ if not volunteers:
+ raise HTTPException(status_code=404, detail="No volunteers found")
+
+ try:
+ images = card_service.generate_multiple_cards(volunteers)
+ pdf_bytes = pdf_service.create_pdf(images)
+ return Response(content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=volunteers.pdf"})
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/{name}/card")
+def get_volunteer_card(
+ name: str,
+ repo: VolunteerRepository = Depends(get_volunteer_repository),
+ card_service: CardGeneratorService = Depends(get_card_generator_service)
+):
+ volunteer = repo.get_by_name(name)
+ if not volunteer:
+ raise HTTPException(status_code=404, detail="Volunteer not found")
+
+ try:
+ img_bytes = card_service.generate_card_png_bytes(volunteer)
+ return Response(content=img_bytes, media_type="image/png")
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/services/api_service.py b/services/api_service.py
new file mode 100644
index 0000000..33c8cb3
--- /dev/null
+++ b/services/api_service.py
@@ -0,0 +1,32 @@
+import requests
+import json
+import os
+from config.settings import settings
+
+BASE_URL = settings.BASE_URL
+SERVICE_TOKEN = settings.SERVICE_TOKEN
+
+class APIService:
+ def __init__(self):
+ self.base_url = BASE_URL
+ self.token = SERVICE_TOKEN
+
+ def _send_request(self, endpoint: str, bearer: bool = True):
+ url = self.base_url + endpoint
+ headers = {}
+ if bearer:
+ headers["Authorization"] = "Bearer " + self.token
+
+ r = requests.get(url, headers=headers)
+ r.raise_for_status() # Good practice to raise on error
+ return r.json()
+
+ def get_hackeps_event_id(self):
+ return self._send_request("/v1/event/get_hackeps")["id"]
+
+ def get_accepted_hackers(self):
+ event_id = self.get_hackeps_event_id()
+ return self._send_request(f'/v1/event/{event_id}/get_approved_hackers')
+
+ def get_companies_by_tier(self, tier):
+ return self._send_request(f'/v1/company/tier/{tier}/')
diff --git a/services/card_generator_service.py b/services/card_generator_service.py
new file mode 100644
index 0000000..d3069aa
--- /dev/null
+++ b/services/card_generator_service.py
@@ -0,0 +1,39 @@
+from typing import List, Any
+from PIL import Image
+from io import BytesIO
+
+class CardGeneratorService:
+ """Service for generating identification cards and converting them to various formats"""
+
+ def generate_card_image(self, user: Any) -> Image.Image:
+ """Generate a card image for a single user"""
+ user.generate_card()
+ if not user.card:
+ raise ValueError(f"Failed to generate card for user {user.id}")
+ return user.card
+
+ def generate_card_png_bytes(self, user: Any) -> bytes:
+ """Generate a card and return it as PNG bytes"""
+ card_image = self.generate_card_image(user)
+ img_byte_arr = BytesIO()
+ card_image.save(img_byte_arr, format='PNG')
+ return img_byte_arr.getvalue()
+
+ def generate_multiple_cards(self, users: List[Any]) -> List[Image.Image]:
+ """Generate cards for multiple users and return list of images"""
+ if not users:
+ raise ValueError("No users provided")
+
+ images = []
+ for user in users:
+ try:
+ card = self.generate_card_image(user)
+ images.append(card)
+ except ValueError:
+ # Skip users that fail to generate cards
+ continue
+
+ if not images:
+ raise ValueError("Failed to generate any card images")
+
+ return images
diff --git a/services/pdf_service.py b/services/pdf_service.py
new file mode 100644
index 0000000..ffca685
--- /dev/null
+++ b/services/pdf_service.py
@@ -0,0 +1,104 @@
+from PIL import Image
+from fpdf import FPDF
+import os
+import tempfile
+import shutil
+
+class PDFService:
+ def __init__(self):
+ # Config params from batch_pdfer.py
+ self.PIXEL_TO_MM = 0.0847
+ self.IMAGES_PER_ROW = 3
+ self.IMAGES_PER_COL = 3
+ self.MARGIN_MM = 0
+ self.GAP_MM = 1
+ self.TARGET_WIDTH_MM = 95
+ self.TARGET_HEIGHT_MM = 70
+ self.AUTO_LAYOUT = False
+ self.CELL_FILL = 0.95
+ self.UPSCALE = True
+ self.GLOBAL_SCALE = 1.0
+
+ def create_pdf(self, images: list[Image.Image]) -> str:
+ """
+ Creates a PDF from a list of PIL Images.
+ Returns the path to the generated PDF file.
+ """
+ # Create a temp dir to store images
+ temp_dir = tempfile.mkdtemp()
+ image_paths = []
+
+ try:
+ # Save all images to temp dir
+ for idx, img in enumerate(images):
+ img_path = os.path.join(temp_dir, f"img_{idx}.png")
+ img.save(img_path)
+ image_paths.append(img_path)
+
+ pdf = FPDF()
+
+ pdf_size = {'P': {'w': 210, 'h': 297}, 'L': {'w': 297, 'h': 210}}
+
+ for index, imageFile in enumerate(image_paths):
+ # Retrieve dimensions from stored file to match original logic
+ # although we have the PIL objects, fpdf needs reading sizing sometimes or we calculate it.
+ # using the PIL object we have in 'images' list would be faster for size,
+ # but let's stick to the file loop logic for consistency with original script if needed.
+ # Actually, we can use the PIL image from the list for sizing.
+
+ curr_img = images[index]
+ px_w, px_h = curr_img.size
+
+ # convert pixels to mm
+ width_mm = float(px_w * self.PIXEL_TO_MM)
+ height_mm = float(px_h * self.PIXEL_TO_MM)
+
+ # determine orientation by image aspect
+ orientation = 'P' if width_mm < height_mm else 'L'
+
+ page_w = pdf_size[orientation]['w']
+ page_h = pdf_size[orientation]['h']
+
+ # compute available cell size
+ cell_w = (page_w - 2 * self.MARGIN_MM - (self.IMAGES_PER_ROW - 1) * self.GAP_MM) / self.IMAGES_PER_ROW
+ cell_h = (page_h - 2 * self.MARGIN_MM - (self.IMAGES_PER_COL - 1) * self.GAP_MM) / self.IMAGES_PER_COL
+
+ if self.TARGET_WIDTH_MM and self.TARGET_HEIGHT_MM:
+ target_w = self.TARGET_WIDTH_MM * self.GLOBAL_SCALE * self.CELL_FILL
+ target_h = self.TARGET_HEIGHT_MM * self.GLOBAL_SCALE * self.CELL_FILL
+ base_scale = min(target_w / width_mm, target_h / height_mm)
+ else:
+ base_scale = min(cell_w / width_mm, cell_h / height_mm)
+
+ if not self.UPSCALE:
+ base_scale = min(1.0, base_scale)
+
+ render_w = width_mm * base_scale
+ render_h = height_mm * base_scale
+
+ # add new page when needed
+ images_per_page = self.IMAGES_PER_ROW * self.IMAGES_PER_COL
+ if index % images_per_page == 0:
+ pdf.add_page(orientation=orientation)
+
+ col = index % self.IMAGES_PER_ROW
+ row = (index // self.IMAGES_PER_ROW) % self.IMAGES_PER_COL
+
+ x = self.MARGIN_MM + (render_w + self.GAP_MM) * col
+ y = self.MARGIN_MM + (render_h + self.GAP_MM) * row
+
+ pdf.image(imageFile, x, y, render_w, render_h)
+
+ output_pdf_path = os.path.join(temp_dir, "output.pdf")
+ pdf.output(output_pdf_path, "F")
+
+ # We need to return the file path, but the temp dir will be tricky if we want to delete it effectively.
+ # Ideally, we return the bytes.
+
+ with open(output_pdf_path, "rb") as f:
+ pdf_bytes = f.read()
+
+ finally:
+ shutil.rmtree(temp_dir)
+
+ return pdf_bytes
diff --git a/test.py b/test.py
deleted file mode 100644
index ee2f8d2..0000000
--- a/test.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from reportlab.pdfgen import canvas
-from reportlab.lib.pagesizes import A4, landscape
-from reportlab.lib.units import mm
-from reportlab.lib.utils import ImageReader
-
-from PIL import Image
-
-import os
-
-path = '_out_'
-filename = 'test.pdf'
-title = 'Test'
-
-MAXY = 139
-MAXX = -5
-MARGIN = 10
-
-def createPDF(path_to_images, document_name, document_title):
-
- def rowGen(list_of_images): #Creates a list of 4 image rows
-
- for i in range(0, len(list_of_images), 2):
- yield list_of_images[i:i + 2]
-
-
- def renderRow(path_to_images, row, y_pos): #Renders each row to the page
-
- x_pos = 27.5 #starting x position
- thumb_size = 450, 400 #Thumbnail image size
-
- for i in row:
-
- img = Image.open(os.path.join(path_to_images, i)) #Opens image as a PIL object
- img.thumbnail(thumb_size) #Creates thumbnail
-
- img = ImageReader(img) #Passes PIL object to the Reportlab ImageReader
-
- #Lays out the image and filename
- pdf.drawImage(img, x_pos * mm , y_pos * mm, width = 317, height = 227, preserveAspectRatio=True, anchor='c')
-
- x_pos += 150 #Increments the x position ready for the next image
-
-
- images = [i for i in os.listdir(path_to_images) if i.endswith('.png')] #Creates list of images filtering out non .jpgs
- row_layout = list(rowGen(images)) #Creates the layout of image rows
-
- pdf = canvas.Canvas(document_name, pagesize=landscape(A4), pageCompression=1) #Creates the PDF object
- pdf.setTitle(document_title)
-
- rows_rendered = 0
-
- y_pos = 113.5 #Sets starting y pos
- pdf.setFont('Helvetica', 10)
-
- for row in row_layout: #Loops through each row in the row_layout list and renders the row. For each 5 rows, makes a new page
-
- if rows_rendered == 2:
-
- pdf.showPage()
- pdf.setFont('Helvetica', 10)
- y_pos = -100
- rows_rendered = 0
- break
-
- else:
-
- renderRow(path_to_images, row, y_pos)
- y_pos -= 100
- rows_rendered += 1
- pdf.save()
-createPDF(path, filename, title)
\ No newline at end of file
diff --git a/tools.py b/tools.py
new file mode 100644
index 0000000..5035b57
--- /dev/null
+++ b/tools.py
@@ -0,0 +1,231 @@
+from base64 import b64decode
+import hashlib
+from io import BytesIO
+import json
+import os
+
+import qrcode
+from PIL import ImageDraw, Image
+import requests
+from config.constants import BAK_COLOR
+
+
+
+
+
+def draw_text(image, text, pos, font, fill, centrate=True, mayus=False):
+ if mayus:
+ text = text.upper()
+ draw = ImageDraw.Draw(image)
+ w = draw.textlength(text, font=font)
+ # Center horizontally around pos[0] when centrate=True
+ if centrate:
+ x = pos[0] - (w / 2)
+ ImageDraw.Draw(image).text((x, pos[1]), text, font=font, fill=fill)
+ else:
+ ImageDraw.Draw(image).text(pos, text, font=font, fill=fill)
+
+def centrate_text_relative(image, text, font, relative_pos, relative_size, fill, mayus=False):
+ #centrate relative on x and split in 2 lines if text width is bigger than relative_size
+ if mayus:
+ text = text.upper()
+ draw = ImageDraw.Draw(image)
+ w = draw.textlength(text, font=font)
+ # center the text horizontally around relative_pos[0]
+ x = relative_pos[0] - (w / 2)
+ y = relative_pos[1]
+ # if w > relative_size[0]:
+ # #split in 2 lines
+ # words = text.split(' ')
+ # line1 = ''
+ # line2 = ''
+ # for word in words:
+ # if draw.textsize(line1 + ' ' + word, font=font)[0] < relative_size[0]:
+ # line1 += ' ' + word
+ # else:
+ # line2 += ' ' + word
+ # x1 = relative_pos[0] + (relative_size[0] - draw.textsize(line1, font=font)[0]) / 2
+ # x2 = relative_pos[0] + (relative_size[0] - draw.textsize(line2, font=font)[0]) / 2
+ # draw.text((x1, y), line1, font=font)
+ # draw.text((x2, y + h), line2, font=font)
+ # else:
+ ImageDraw.Draw(image).text((x, y), text, font=font, fill=fill)
+
+def has_transparency(img):
+ if img.info.get("transparency", None) is not None:
+ return True
+ if img.mode == "P":
+ transparent = img.info.get("transparency", -1)
+ for _, index in img.getcolors():
+ if index == transparent:
+ return True
+ elif img.mode == "RGBA":
+ extrema = img.getextrema()
+ if extrema[3][0] < 255:
+ return True
+
+ return False
+
+def scale(image, max_size, mask_col=BAK_COLOR, method=Image.LANCZOS):
+ """
+ resize 'image' to 'max_size' keeping the aspect ratio
+ and place it in center of white 'max_size' image
+ """
+ im_aspect = float(image.size[0]) / float(image.size[1])
+ out_aspect = float(max_size[0]) / float(max_size[1])
+ if im_aspect >= out_aspect:
+ scaled = image.resize((max_size[0], int((float(max_size[0]) / im_aspect) + 0.5)), method)
+ else:
+ scaled = image.resize((int((float(max_size[1]) * im_aspect) + 0.5), max_size[1]), method)
+
+ offset = (((max_size[0] - scaled.size[0]) / 2), ((max_size[1] - scaled.size[1]) / 2))
+ back = Image.new("RGBA", max_size, mask_col)
+ if has_transparency(image):
+ try:
+ back.paste(scaled, (int(offset[0]), int(offset[1])), scaled)
+ except:
+ scaled = scaled.convert("RGBA")
+ back.paste(scaled, (int(offset[0]), int(offset[1])), scaled)
+ else:
+ back.paste(scaled, (int(offset[0]), int(offset[1])))
+ return back
+
+def generate_qr(input, size, border_size):
+ qr = qrcode.QRCode(
+ version=2,
+ error_correction=qrcode.constants.ERROR_CORRECT_H,
+ box_size=size,
+ border=border_size)
+ qr.add_data(input)
+ qr.make(fit=True)
+ qr = qr.make_image()
+ return qr
+
+
+def crypt(input, method='md5'):
+ m = hashlib.new(method)
+ m.update(bytes(input, 'utf'))
+ return str(m.digest())
+
+
+class DataFile:
+ __contents = {}
+
+ @staticmethod
+ def get_content(filepath, type, reload_if_cached=False):
+ if not reload_if_cached and filepath in DataFile.__contents:
+ return DataFile.__contents[filepath]
+ else:
+ with open(filepath, 'r', encoding='utf-8') as json_file:
+ if type == 'JSON':
+ data = json.load(json_file)
+ DataFile.__contents[filepath] = data
+ return data
+ else:
+ raise Exception(NotImplemented)
+
+ @staticmethod
+ def clear_cached_content(filepath):
+ if filepath in DataFile.__contents:
+ del DataFile.__contents[filepath]
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def clear_cache():
+ DataFile.__contents = {}
+
+
+def translate_image(image) -> Image:
+ # handle bytes directly
+ if isinstance(image, (bytes, bytearray)):
+ return Image.open(BytesIO(image))
+
+ s = str(image)
+ # remote URL
+ if s[:4].lower() == "http":
+ resp = requests.get(s, timeout=10)
+ try:
+ resp.raise_for_status()
+ except Exception as e:
+ raise RuntimeError(f"Failed to download image from URL {s}: {e}")
+
+ ct = resp.headers.get('Content-Type', '').lower()
+ content = resp.content
+
+ # handle SVG responses specifically (PIL doesn't support SVG)
+ if 'svg' in ct or (content.lstrip().startswith(b'<') and b'