cteward-ng/cteward_ng/views.py

274 lines
9.9 KiB
Python
Raw Permalink Normal View History

2026-06-06 10:18:15 +02:00
"""Legacy API route handlers.
Replaces all server.get() calls in startup.js.
Blueprint URL prefix: /legacy
2026-06-08 20:33:47 +02:00
Each endpoint follows the same pipeline:
1. Build permissions dict (role {query, level, filter?})
2. auth.authorize(ctx) sets username, flags, query, filter
3. database.run_query(query_def, params) executes SQL
4. filters.runfilter(ctx) applies permission-level data filter
5. mappings.*_mapper(ctx) transforms raw rows into API shape
6. renderers.json_output / csv_output(ctx) returns HTTP response
2026-06-06 10:18:15 +02:00
"""
2026-06-08 20:33:47 +02:00
from flask import Blueprint, current_app, request, jsonify
import auth
import database as db
import filters
import mappings
import renderers
2026-06-06 10:18:15 +02:00
legacy_bp = Blueprint('legacy', __name__)
2026-06-08 20:33:47 +02:00
# ── helper to build the context dict for authorize ───────────────
def _ctx(request, permissions):
"""Build a context dict ready for auth.authorize()."""
return {
'config': current_app.cteward_config,
'request': request,
'permissions': permissions,
}
# ── /monitor ────────────────────────────────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/monitor')
def monitor():
"""Health check endpoint."""
2026-06-08 20:33:47 +02:00
try:
db.check_backend_okay()
return jsonify({'status': 'OK'})
except Exception as exc:
return jsonify({'status': 'BROKEN'}), 503
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
# ── /memberlist-oldformat (CSV) ────────────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/memberlist-oldformat')
def memberlist_oldformat():
2026-06-08 20:33:47 +02:00
"""CSV member list for LDAP export.
Permissions:
board/bots : full MEMBERLIST query, level 0
members : active-only filter, level 1
passive : self-only filter, level 2
astronauts: self-only filter, level 3
"""
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_MEMBERLIST, 'level': 0},
'_bot_': {'query': db.QUERY_MEMBERLIST, 'level': 0},
'_member_': {'query': db.QUERY_MEMBERLIST, 'level': 1, 'filter': filters.memberlist_active_only},
'_passive_': {'query': db.QUERY_MEMBERLIST, 'level': 2, 'filter': filters.memberlist_self_only},
'_astronaut_': {'query': db.QUERY_MEMBERLIST, 'level': 3, 'filter': filters.memberlist_self_only},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {'crewname': request.view_args.get('crewname', '')})
filters.runfilter(ctx)
mappings.memberlist_to_ldapcsv_mapper(ctx)
return renderers.csv_output(ctx)
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
# ── /stats/* ───────────────────────────────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/stats/members')
def stats_members():
2026-06-08 20:33:47 +02:00
"""Member count time series."""
ctx = _ctx(request, {
'_bot_': {'query': db.QUERY_STATS_MEMBERS, 'level': 0},
'_member_': {'query': db.QUERY_STATS_MEMBERS, 'level': 0},
'_stats_': {'query': db.QUERY_STATS_MEMBERS, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {})
filters.runfilter(ctx)
return renderers.json_output(ctx)
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/stats/contracts')
def stats_contracts():
2026-06-08 20:33:47 +02:00
"""Contract statistics time series."""
ctx = _ctx(request, {
'_bot_': {'query': db.QUERY_STATS_CONTRACTS, 'level': 0},
'_board_': {'query': db.QUERY_STATS_CONTRACTS, 'level': 0},
'_stats_': {'query': db.QUERY_STATS_CONTRACTS, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {})
filters.runfilter(ctx)
return renderers.json_output(ctx)
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/stats/genders')
def stats_genders():
2026-06-08 20:33:47 +02:00
"""Gender demographics time series."""
ctx = _ctx(request, {
'_bot_': {'query': db.QUERY_STATS_GENDERS, 'level': 0},
'_board_': {'query': db.QUERY_STATS_GENDERS, 'level': 0},
'_stats_': {'query': db.QUERY_STATS_GENDERS, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {})
filters.runfilter(ctx)
return renderers.json_output(ctx)
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/stats/ages')
def stats_ages():
2026-06-08 20:33:47 +02:00
"""Age demographics time series.
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
Accepts query params: step, min, max for age bucket configuration.
"""
ctx = _ctx(request, {
'_bot_': {'query': db.QUERY_STATS_AGES, 'level': 0},
'_board_': {'query': db.QUERY_STATS_AGES, 'level': 0},
'_stats_': {'query': db.QUERY_STATS_AGES, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], dict(request.args))
filters.runfilter(ctx)
return renderers.json_output(ctx)
# ── /member/<crewname> ───────────────────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/member/<crewname>')
def member(crewname):
"""Member details (or list when crewname is '' or '*')."""
2026-06-08 20:33:47 +02:00
if crewname in ('', '*'):
# Return a list with permission-based filtering
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_MEMBERLIST, 'level': 0},
'_member_': {'query': db.QUERY_MEMBERLIST, 'level': 1, 'filter': filters.memberlist_active_only},
'_passive_': {'query': db.QUERY_MEMBERLIST, 'level': 2, 'filter': filters.memberlist_self_only},
'_astronaut_': {'query': db.QUERY_MEMBERLIST, 'level': 2, 'filter': filters.memberlist_self_only},
})
else:
# Return single member record
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_MEMBER_BY_CREWNAME, 'level': 0},
'_self_': {'query': db.QUERY_MEMBER_BY_CREWNAME, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname})
filters.runfilter(ctx)
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
# Use MEMBERLIST mapper for list queries, MEMBER mapper for single record
if crewname in ('', '*'):
mappings.memberlist_mapper(ctx)
else:
mappings.member_mapper(ctx)
return renderers.json_output(ctx)
# ── /member/<crewname>/raw ───────────────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/member/<crewname>/raw')
def member_raw(crewname):
2026-06-08 20:33:47 +02:00
"""Raw DB member record (board/bots only)."""
if crewname in ('', '*'):
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_MEMBERLIST_RAW, 'level': 0},
})
else:
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_MEMBER_BY_CREWNAME, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname})
filters.runfilter(ctx)
mappings.none_mapper(ctx)
return renderers.json_output(ctx)
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
# ── /member/<crewname>/memo ───────────────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/member/<crewname>/memo')
def member_memo(crewname):
2026-06-08 20:33:47 +02:00
"""RTF memo for a member (board only)."""
if not crewname:
return jsonify({'error': 'Member details only displayed for individual members.'}), 400
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_MEMBER_MEMO_BY_CREWNAME, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname})
filters.runfilter(ctx)
mappings.memo_mapper(ctx)
return renderers.json_output(ctx)
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
# ── /member/<crewname>/contributions ─────────────────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/member/<crewname>/contributions')
def member_contributions(crewname):
2026-06-08 20:33:47 +02:00
"""Contribution summary for a member (board only)."""
if not crewname:
return jsonify({'error': 'Member details only displayed for individual members.'}), 400
ctx = _ctx(request, {
'_board_': {'query': db.QUERY_DEBITLIST_BY_CREWNAME, 'level': 0},
})
auth.authorize(ctx)
ctx['data'] = db.run_query(ctx['query'], {'crewname': crewname})
filters.runfilter(ctx)
mappings.contributions_mapper(ctx)
return renderers.json_output(ctx)
2026-06-06 10:18:15 +02:00
2026-06-08 20:33:47 +02:00
# ── /member/<crewname>/<detail_type>/[<id>]/raw/ ─────────────
2026-06-06 10:18:15 +02:00
@legacy_bp.route('/member/<crewname>/<detail_type>/raw/')
@legacy_bp.route('/member/<crewname>/<detail_type>/<detail_id>/raw/')
def member_detail_raw(crewname, detail_type, detail_id=None):
2026-06-08 20:33:47 +02:00
"""Raw detail records (contract, debit, withdrawal, payment).
Permissions: board and self can see their own data.
"""
if crewname == '*':
return jsonify({'error': 'Member details only displayed for individual members.'}), 400
# Resolve the list query and the detail-id query from the type
DETAIL_QUERIES = {
'contract': (db.QUERY_CONTRACTLIST_BY_CREWNAME, 'contract', db.QUERY_CONTRACT_BY_CREWNAME_AND_CONTRACT),
'debit': (db.QUERY_DEBITLIST_BY_CREWNAME, 'guid', db.QUERY_DEBIT_BY_CREWNAME_AND_GUID),
'withdrawal': (db.QUERY_WITHDRAWALLIST_BY_CREWNAME, 'guid', db.QUERY_WITHDRAWAL_BY_CREWNAME_AND_GUID),
'payment': (db.QUERY_PAYMENTLIST_BY_CREWNAME, None, None), # no ID sub-query for payment
}
info = DETAIL_QUERIES.get(detail_type)
if info is None:
return jsonify({'error': 'Invalid member detail requested.'}), 400
list_query, id_param, id_query = info
perms = {
'_board_': {'query': list_query, 'level': 0},
}
ctx = _ctx(request, perms)
auth.authorize(ctx)
if id_param and detail_id and detail_id not in ('', '*'):
# Fetch single record by ID
perms['_board_']['query'] = id_query
params = {'crewname': crewname}
params[id_param] = detail_id
else:
# Fetch list
params = {'crewname': crewname}
ctx['data'] = db.run_query(
perms['_board_']['query'],
params,
)
filters.runfilter(ctx)
mappings.none_mapper(ctx)
return renderers.json_output(ctx)