from __future__ import annotations from collections.abc import Sequence from datetime import datetime, timedelta from sqlalchemy import Select from sqlalchemy.orm import selectinload from app.extensions import db from app.models import AuditLog def audit_log_query( *, actor_user_id: int | None = None, action_type: str | None = None, from_date: str | None = None, to_date: str | None = None, ) -> Select[tuple[AuditLog]]: query = db.select(AuditLog).options(selectinload(AuditLog.actor)) if actor_user_id is not None: query = query.where(AuditLog.actor_user_id == actor_user_id) normalized_action_type = (action_type or "").strip() if normalized_action_type: query = query.where(AuditLog.action_type == normalized_action_type) parsed_from_date = _parse_date_start(from_date) if parsed_from_date is not None: query = query.where(AuditLog.created_at >= parsed_from_date) parsed_to_date = _parse_date_end(to_date) if parsed_to_date is not None: query = query.where(AuditLog.created_at < parsed_to_date) return query def list_audit_logs( *, actor_user_id: int | None = None, action_type: str | None = None, from_date: str | None = None, to_date: str | None = None, limit: int = 100, ) -> Sequence[AuditLog]: result = db.session.execute( audit_log_query( actor_user_id=actor_user_id, action_type=action_type, from_date=from_date, to_date=to_date, ) .order_by(AuditLog.created_at.desc(), AuditLog.id.desc()) .limit(limit), ) return result.scalars().all() def get_audit_log_or_none(log_id: int) -> AuditLog | None: result = db.session.execute( db.select(AuditLog).options(selectinload(AuditLog.actor)).where(AuditLog.id == log_id), ) return result.scalar_one_or_none() def list_action_types() -> list[str]: result = db.session.execute( db.select(AuditLog.action_type).distinct().order_by(AuditLog.action_type.asc()), ) return [value for value in result.scalars().all() if value] def _parse_date_start(raw_value: str | None) -> datetime | None: value = (raw_value or "").strip() if not value: return None try: return datetime.fromisoformat(value) except ValueError: return None def _parse_date_end(raw_value: str | None) -> datetime | None: start = _parse_date_start(raw_value) if start is None: return None return start + timedelta(days=1)