Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Architecture

A note about this codebase: This was originally taken from Auth0 and messages_views.py and messages_service.py were the original files we built from. Over time we have created a better structure for this backend service. Please do not update these two files anymore, you should consider the other code that exists first, and then add a new _views.py and a new _service.py if you're unable to find a good place to update the code as it relates to the file. If you find code in these two "messages" files that you plan to update, it's encouraged to migrate that code elsewhere to help iteratively move away from messages_views and messages_service.

### Flask app with Blueprint-based API modules
The app is created via `api/__init__.py:create_app()`. Each API domain is a Blueprint registered there. The WSGI entrypoint is `api/wsgi.py`.

Expand Down Expand Up @@ -117,6 +119,13 @@ To send a Slack DM, pass the Slack user ID as `channel`: `send_slack(message=...

These are DIFFERENT VALUES. When bundling user data for the frontend, include both: `{user_id: propel_id, db_id: firestore_doc_id, name, profile_image}`. The frontend needs `db_id` to build profile links and `user_id` (propel) for matching against assignees/editors/mentions.

## Admin Email Templates (`email_templates` collection)
Powers the frontend `/admin/communication` template editor and the volunteer send-email dialogs. Blueprint: `api/email_templates/email_templates_views.py` (`/api/admin/templates*`, all `volunteer.admin`-gated); service: `services/email_templates_service.py`; seed data: `services/email_templates_seed.py`.
- Layout: `email_templates/{slug}` main doc + `email_templates/{slug}/versions/{000N}` append-only content snapshots. `version` on the main doc = current version number; version doc ids are zero-padded for natural ordering.
- Versioning rules: content-key edits (`title/category/category_key/applicable_roles/message/icon`) bump version + append a snapshot; status-only patches don't bump; **revert never rewrites history** — it copies the target version's content forward as a NEW version (`change_note: "Reverted to version N"`).
- Seeding: `email_templates_seed.py` holds the original 22 hardcoded frontend templates (GENERATED from frontend `src/lib/messageTemplates.js` — regenerate, don't hand-edit; editing it does not change live emails). Auto-seeds on first list call when the collection is empty; `POST /api/admin/templates/seed` re-inserts missing seed docs only — never overwrites admin edits.
- DELETE is a hard delete (doc + versions). Deleted seed templates can be restored via /seed (history restarts at v1).

## Hackathon roster import + audit scripts
Two scripts live in `scripts/` for diagnosing and backfilling team rosters on `/hack/<event_id>`:

Expand Down
2 changes: 2 additions & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ def add_headers(response):
from api.store import store_views
from api.planning import planning_views
from api.mentors import mentors_views
from api.email_templates import email_templates_views

app.register_blueprint(messages_views.bp)
app.register_blueprint(exception_views.bp)
Expand All @@ -207,5 +208,6 @@ def add_headers(response):
app.register_blueprint(store_views.bp)
app.register_blueprint(planning_views.bp)
app.register_blueprint(mentors_views.bp)
app.register_blueprint(email_templates_views.bp)

return app
Empty file added api/email_templates/__init__.py
Empty file.
97 changes: 97 additions & 0 deletions api/email_templates/email_templates_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Admin email template CRUD with version history.

Powers the /admin/communication template editor on the frontend. Templates
live in the email_templates collection (see services/email_templates_service)
with an append-only versions subcollection for history/revert. Seeded from
the original hardcoded frontend templates on first list call.
"""

from flask import Blueprint, request

from common.log import get_logger
from common.auth import auth, auth_user, getOrgId
from services.email_templates_service import (
admin_list_templates,
admin_create_template,
admin_update_template,
admin_delete_template,
admin_get_template_versions,
admin_revert_template,
admin_seed_templates,
)

logger = get_logger(__name__)

bp = Blueprint("email_templates", __name__, url_prefix="/api")


def _actor_from_request():
try:
return {
"propel_user_id": auth_user.user_id if auth_user else None,
"email": getattr(auth_user, "email", None) if auth_user else None,
}
except Exception:
return None


@bp.route("/admin/templates", methods=["GET"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_get_templates():
logger.info("GET /admin/templates called")
return vars(admin_list_templates())


@bp.route("/admin/templates", methods=["POST"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_post_template():
logger.info("POST /admin/templates called")
msg, status_code = admin_create_template(request.get_json(), _actor_from_request())
return vars(msg), status_code


@bp.route("/admin/templates/seed", methods=["POST"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_post_template_seed():
logger.info("POST /admin/templates/seed called")
msg, status_code = admin_seed_templates()
return vars(msg), status_code


@bp.route("/admin/templates/<template_id>", methods=["PATCH"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_patch_template(template_id):
logger.info(f"PATCH /admin/templates/{template_id} called")
msg, status_code = admin_update_template(template_id, request.get_json(), _actor_from_request())
return vars(msg), status_code


@bp.route("/admin/templates/<template_id>", methods=["DELETE"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_delete_template_route(template_id):
logger.info(f"DELETE /admin/templates/{template_id} called")
msg, status_code = admin_delete_template(template_id)
return vars(msg), status_code


@bp.route("/admin/templates/<template_id>/versions", methods=["GET"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_get_template_versions_route(template_id):
logger.info(f"GET /admin/templates/{template_id}/versions called")
msg, status_code = admin_get_template_versions(template_id)
return vars(msg), status_code


@bp.route("/admin/templates/<template_id>/revert", methods=["POST"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def admin_post_template_revert(template_id):
logger.info(f"POST /admin/templates/{template_id}/revert called")
msg, status_code = admin_revert_template(template_id, request.get_json(), _actor_from_request())
return vars(msg), status_code
Loading
Loading