Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4a2b1e1
Add Resend List Emails API for recipient-based email status lookup
gregv Feb 22, 2026
de319b5
Upgrade resend SDK to 2.22.0 and use Emails.list() natively
gregv Feb 23, 2026
7464ee6
Skip caching empty Resend email results
gregv Feb 24, 2026
dc73cfd
Initial plan
Copilot Mar 6, 2026
5cd313a
Initial plan
Copilot Mar 6, 2026
3f38d5e
Convert resend-list endpoint from GET to POST with JSON body to avoid…
Copilot Mar 6, 2026
39e1f69
Add .github/copilot-instructions.md with repository guidance for GitH…
Copilot Mar 6, 2026
619b661
Initial plan
Copilot Mar 6, 2026
1321abd
Merge pull request #187 from opportunity-hack/feature/resend-list-ema…
gregv Mar 6, 2026
eb496d6
Fix list_all_resend_emails: pagination cap, error handling, empty-res…
Copilot Mar 6, 2026
2de1f45
Merge pull request #188 from opportunity-hack/copilot/sub-pr-187
gregv Mar 6, 2026
3a93525
Merge pull request #190 from opportunity-hack/copilot/set-up-copilot-…
gregv Mar 6, 2026
5cb0314
Merge pull request #191 from opportunity-hack/copilot/sub-pr-187-again
gregv Mar 6, 2026
703abda
Merge pull request #192 from opportunity-hack/feature/resend-list-ema…
gregv Mar 6, 2026
5bf9df5
Add admin endpoints for hackathon request management
gregv Mar 6, 2026
307a5ef
Merge pull request #193 from opportunity-hack/feature/admin-hackathon…
gregv Mar 6, 2026
dfd1f5f
Add admin contact submissions endpoints and generic email tracking
gregv Mar 6, 2026
7ccd9c0
Merge pull request #194 from opportunity-hack/feature/admin-contact-e…
gregv Mar 6, 2026
b045688
Refactor: break up messages_service.py monolith into domain modules
gregv Mar 6, 2026
a5a430d
Extract teams and email services from messages_service monolith
gregv Mar 6, 2026
16ba89e
Extract news/praise, feedback, giveaway, and onboarding services
gregv Mar 6, 2026
80dbc96
Merge pull request #195 from opportunity-hack/refactor/break-up-messa…
gregv Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copilot Instructions for backend-ohack.dev

## Project Overview
This is the Python/Flask backend API for [Opportunity Hack](https://www.ohack.dev) — a nonprofit hackathon platform. It serves the frontend at [ohack.dev](https://www.ohack.dev) and is accessible at [api.ohack.dev](https://api.ohack.dev).

## Tech Stack
- **Language**: Python 3.9.13
- **Framework**: Flask
- **Database**: Google Cloud Firestore (Firebase)
- **Authentication**: PropelAuth (`propelauth_flask`)
- **Caching**: Redis (optional locally)
- **Deployment**: Fly.io
- **Testing**: pytest
- **Linting**: pylint

## Project Structure
```
api/ # Flask blueprints (one subdirectory per feature)
certificates/ # Certificate generation
contact/ # Contact form
github/ # GitHub integration
hearts/ # Kudos/hearts feature
judging/ # Hackathon judging
leaderboard/ # Leaderboard endpoints
llm/ # LLM/AI features
messages/ # Messaging endpoints
newsletters/ # Newsletter management
problemstatements/ # Nonprofit problem statements
slack/ # Slack integration
teams/ # Team management
users/ # User profile endpoints
validate/ # Validation endpoints
volunteers/ # Volunteer management
__init__.py # App factory (create_app)
common/ # Shared utilities
auth.py # PropelAuth setup and helpers
exceptions.py # Custom exception classes
utils/ # Utility helpers
db/ # Database layer
db.py # Main DB module
firestore.py # Firestore client wrapper
interface.py # DB interface/abstraction
mem.py # In-memory caching
model/ # Data models (dataclasses)
services/ # Business logic services
test/ # Integration / service tests
```

## Development Commands
```bash
# Install dependencies
pip install -r requirements.txt

# Run the app (development)
flask run

# Run tests
pytest

# Run a single test
pytest path/to/test_file.py::test_function_name

# Run linter
pylint api/ common/ db/ model/ services/
pylint -E api/*.py # errors only (used in CI)
```

## Environment Setup
Copy `.env.example` to `.env` and fill in real values (obtain secrets from the team Slack channel):
```bash
cp .env.example .env
```

Key environment variables:
- `CLIENT_ORIGIN_URL` — allowed CORS origins (comma-separated or `*` for dev)
- `FIREBASE_CERT_CONFIG` — Firebase service account JSON
- `PROPEL_AUTH_KEY` / `PROPEL_AUTH_URL` — PropelAuth credentials
- `OPENAI_API_KEY` — OpenAI key for LLM features
- `SLACK_BOT_TOKEN` / `SLACK_WEBHOOK` — Slack integration
- `ENC_DEC_KEY` — encryption/decryption key
- `REDIS_URL` — Redis URL (optional for local development)

## Code Style Guidelines
- **Python version**: 3.9.13
- **Naming**: `snake_case` for variables and functions; `PascalCase` for classes
- **Imports**: standard library → third-party → local (one blank line between groups)
- **Type hints**: use `from typing import *` and annotate function parameters and return types
- **Error handling**: use `try/except` with specific exception types; avoid bare `except`
- **Docstrings**: recommended for complex logic, not required for every function
- **Linting**: pylint with `.pylintrc` disabling `missing-module-docstring` (C0114), `missing-function-docstring` (C0116), and `too-few-public-methods` (R0903)

## Architecture Patterns
- **App factory**: `api/__init__.py` exports `create_app()` which registers all Flask blueprints
- **Blueprints**: each feature lives in its own subdirectory under `api/` and registers a Flask `Blueprint`
- **Service layer**: business logic lives in `services/` or in `*_service.py` files inside the blueprint directory, not in view files
- **DB abstraction**: all Firestore access goes through `db/` — do not call Firestore directly from views
- **Auth**: use `common/auth.py`'s `auth` object and `auth_user` / `getOrgId()` helpers for authentication; do not import PropelAuth directly in views
- **Utilities**: use `common/utils/safe_get_env_var()` to read environment variables (never `os.environ[]` directly in business logic)

## Testing Guidelines
- Tests live alongside the code they test inside a `tests/` subdirectory of each blueprint, or in the top-level `test/` directory for integration tests
- Use `pytest` fixtures defined in `conftest.py`
- Mock external services (Firestore, PropelAuth, Slack, OpenAI) in unit tests
- Run the full suite with `pytest` before opening a PR

## CI / CD
- GitHub Actions workflow (`.github/workflows/main.yml`) runs pylint and tests on every push and PR
- Merges to `develop` deploy to the staging environment; merges to `main` deploy to production via Fly.io
49 changes: 49 additions & 0 deletions api/contact/contact_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,55 @@ def verify_recaptcha(token: str) -> bool:
logger.exception("Error verifying recaptcha: %s", str(e))
return False

def get_all_contact_submissions() -> Dict[str, Any]:
"""
Get all contact form submissions for admin viewing.

Returns:
Dict with list of submissions sorted by timestamp desc
"""
try:
db = get_db()
submissions = []
docs = db.collection('contact_submissions').order_by('timestamp', direction='DESCENDING').stream()
for doc in docs:
data = doc.to_dict()
data['id'] = doc.id
submissions.append(data)
return {"success": True, "submissions": submissions}
except Exception as e:
logger.error("Error fetching contact submissions: %s", str(e))
return {"success": False, "error": str(e), "submissions": []}


def admin_update_contact_submission(doc_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Update a contact submission's status and admin notes.

Args:
doc_id: Firestore document ID
data: Dict with status and/or adminNotes

Returns:
Dict with success status
"""
try:
db = get_db()
update_data = {}
if 'status' in data:
update_data['status'] = data['status']
if 'adminNotes' in data:
update_data['adminNotes'] = data['adminNotes']
update_data['updatedAt'] = _get_current_timestamp()

db.collection('contact_submissions').document(doc_id).update(update_data)
logger.info("Updated contact submission %s", doc_id)
return {"success": True}
except Exception as e:
logger.error("Error updating contact submission %s: %s", doc_id, str(e))
return {"success": False, "error": str(e)}


def send_confirmation_email(contact_data) -> bool:
"""
Send a confirmation email to the contact form submitter.
Expand Down
45 changes: 43 additions & 2 deletions api/contact/contact_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from flask import Blueprint, jsonify, request
from common.log import get_logger
from common.exceptions import InvalidInputError
from api.contact.contact_service import submit_contact_form
from common.auth import auth, auth_user
from api.contact.contact_service import (
submit_contact_form,
get_all_contact_submissions,
admin_update_contact_submission,
)

logger = get_logger(__name__)
bp = Blueprint('contact', __name__, url_prefix='/api')
Expand Down Expand Up @@ -87,4 +92,40 @@ def handle_contact_form():
return jsonify({
"success": False,
"error": "An error occurred while processing your request"
}), 500
}), 500


def getOrgId(req):
return req.headers.get("X-Org-Id")


@bp.route("/contact/submissions", methods=["GET"])
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_list_contact_submissions():
"""Admin endpoint to list all contact form submissions."""
try:
result = get_all_contact_submissions()
if result.get('success'):
return jsonify(result), 200
return jsonify(result), 500
except Exception as e:
logger.exception("Error listing contact submissions: %s", str(e))
return jsonify({"success": False, "error": str(e)}), 500


@bp.route("/contact/submissions/<submission_id>", methods=["PATCH"])
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_update_contact_submission_route(submission_id):
"""Admin endpoint to update a contact submission's status and notes."""
try:
data = request.get_json()
if not data:
return jsonify({"success": False, "error": "Empty request body"}), 400

result = admin_update_contact_submission(submission_id, data)
if result.get('success'):
return jsonify(result), 200
return jsonify(result), 500
except Exception as e:
logger.exception("Error updating contact submission: %s", str(e))
return jsonify({"success": False, "error": str(e)}), 500
4 changes: 2 additions & 2 deletions api/judging/judging_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
from model.judge_assignment import JudgeAssignment
from model.judge_score import JudgeScore
from model.judge_panel import JudgePanel
from api.messages.messages_service import (
get_single_hackathon_event,
from services.hackathons_service import get_single_hackathon_event
from services.teams_service import (
get_team,
get_teams_batch
)
Expand Down
Loading