mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 05:10:26 -05:00
5df748b30e
- Add POST /api/v1/time-entries/import-csv and POST /api/v1/time-entries/bulk. - Support Idempotency-Key on POST /api/v1/time-entries with 24h replay per token. - Enforce per-token minute/hour limits (Redis when available, local fallback otherwise). - Extract bulk and CSV import logic into dedicated services; trim legacy api route surface.
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""Bulk time entry actions shared by legacy session API and API v1."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from app import db
|
|
from app.models import TimeEntry
|
|
from app.models.time_entry import local_now
|
|
from app.utils.db import safe_commit
|
|
|
|
|
|
def apply_bulk_time_entry_actions(
|
|
entry_ids: List[int],
|
|
action: str,
|
|
value: Any,
|
|
*,
|
|
user_id: int,
|
|
is_admin: bool,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Apply bulk action to time entries. Same rules as legacy /api/entries/bulk.
|
|
|
|
Returns dict with keys: success (bool), affected (int), error (optional str),
|
|
http_status (int).
|
|
"""
|
|
if not entry_ids:
|
|
return {"success": False, "error": "entry_ids must be a non-empty list", "http_status": 400}
|
|
if action not in {"delete", "set_billable", "set_paid", "add_tag", "remove_tag"}:
|
|
return {"success": False, "error": "Unsupported action", "http_status": 400}
|
|
|
|
q = TimeEntry.query.filter(TimeEntry.id.in_(entry_ids))
|
|
entries = q.all()
|
|
if not entries:
|
|
return {"success": False, "error": "No entries found", "http_status": 404}
|
|
|
|
if not is_admin:
|
|
for e in entries:
|
|
if e.user_id != user_id:
|
|
return {"success": False, "error": "Access denied for one or more entries", "http_status": 403}
|
|
|
|
affected = 0
|
|
if action == "delete":
|
|
for e in entries:
|
|
if e.is_active:
|
|
continue
|
|
db.session.delete(e)
|
|
affected += 1
|
|
elif action == "set_billable":
|
|
flag = bool(value)
|
|
for e in entries:
|
|
if e.is_active:
|
|
continue
|
|
e.billable = flag
|
|
e.updated_at = local_now()
|
|
affected += 1
|
|
elif action == "set_paid":
|
|
flag = bool(value)
|
|
for e in entries:
|
|
if e.is_active:
|
|
continue
|
|
e.set_paid(flag)
|
|
affected += 1
|
|
elif action in {"add_tag", "remove_tag"}:
|
|
tag = (value or "").strip() if value is not None else ""
|
|
if not tag:
|
|
return {"success": False, "error": "Tag value is required", "http_status": 400}
|
|
for e in entries:
|
|
if e.is_active:
|
|
continue
|
|
tags = set(e.tag_list)
|
|
if action == "add_tag":
|
|
tags.add(tag)
|
|
else:
|
|
tags.discard(tag)
|
|
e.tags = ", ".join(sorted(tags)) if tags else None
|
|
e.updated_at = local_now()
|
|
affected += 1
|
|
|
|
if affected > 0:
|
|
if not safe_commit("bulk_time_entries", {"action": action, "count": affected}):
|
|
return {"success": False, "error": "Database error during bulk operation", "http_status": 500}
|
|
else:
|
|
db.session.rollback()
|
|
if entries:
|
|
return {
|
|
"success": False,
|
|
"error": "No entries were updated; active (running) time entries cannot be changed with this bulk action",
|
|
"http_status": 400,
|
|
"affected": 0,
|
|
}
|
|
|
|
return {"success": True, "affected": affected, "http_status": 200}
|