Files
TimeTracker/app/routes/api_v1_common.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

97 lines
3.0 KiB
Python

"""
Shared helpers for API v1 routes.
Used by api_v1.py and by domain-specific sub-blueprints (e.g. api_v1_time_entries).
"""
from flask import g, jsonify, request
def paginate_query(query, page=None, per_page=None):
"""Paginate a SQLAlchemy query."""
page = page or int(request.args.get("page", 1))
per_page = per_page or int(request.args.get("per_page", 50))
per_page = min(per_page, 100)
paginated = query.paginate(page=page, per_page=per_page, error_out=False)
return {
"items": paginated.items,
"pagination": {
"page": paginated.page,
"per_page": paginated.per_page,
"total": paginated.total,
"pages": paginated.pages,
"has_next": paginated.has_next,
"has_prev": paginated.has_prev,
"next_page": paginated.page + 1 if paginated.has_next else None,
"prev_page": paginated.page - 1 if paginated.has_prev else None,
},
}
def parse_datetime(dt_str):
"""Parse datetime string from API request."""
if not dt_str:
return None
try:
from app.utils.timezone import utc_to_local
ts = dt_str.strip()
if ts.endswith("Z"):
ts = ts[:-1] + "+00:00"
from datetime import datetime
dt = datetime.fromisoformat(ts)
if dt.tzinfo is not None:
dt = utc_to_local(dt).replace(tzinfo=None)
return dt
except Exception:
return None
def _parse_date(dstr):
"""Parse a YYYY-MM-DD string to date."""
if not dstr:
return None
try:
from datetime import date as _date
return _date.fromisoformat(str(dstr))
except Exception:
return None
def _parse_date_range(start_date_str, end_date_str):
"""Parse start/end date params. Date-only end_date becomes end-of-day."""
start_dt = parse_datetime(start_date_str) if start_date_str else None
end_dt = parse_datetime(end_date_str) if end_date_str else None
if end_date_str and end_dt and "T" not in end_date_str.strip() and " " not in end_date_str.strip():
end_dt = end_dt.replace(hour=23, minute=59, second=59, microsecond=999999)
return start_dt, end_dt
def _require_module_enabled_for_api(module_id: str):
"""Return a Flask response tuple if module is disabled for this API user; otherwise None."""
try:
from app.models import Settings
from app.utils.module_registry import ModuleRegistry
settings = Settings.get_settings()
user = getattr(g, "api_user", None)
if not ModuleRegistry.is_enabled(module_id, settings, user):
return (
jsonify(
{
"error": "module_disabled",
"message": f"{module_id} module is disabled by the administrator.",
"error_code": "forbidden",
}
),
403,
)
except Exception:
pass
return None