"""Response serializers — the {"data": ...} envelope lives in the routers.""" from datetime import date, datetime from app.utils.jalali import to_jalali_str def _iso(value: datetime | date | None) -> str | None: if value is None: return None if isinstance(value, datetime): return value.isoformat() return value.isoformat() def user_payload(ctx) -> dict: """The authenticated user as the frontend sees them: identity, role and the permissions/scope the UI derives its navigation from (see frontend src/lib/roles.ts). The API enforces the same values server-side.""" user = ctx.user return { "id": str(user.id), "username": user.username, "display_name": user.display_name, **ctx.as_dict(), } def project_dict(project, member_count: int = 0, task_stats: dict | None = None) -> dict: return { "id": str(project.id), "name": project.name, "description": project.description or "", "location": project.location or "", "project_manager": project.project_manager or "", "planned_start": _iso(project.planned_start), "planned_end": _iso(project.planned_end), "actual_start": _iso(project.actual_start), "actual_end": _iso(project.actual_end), "planned_start_jalali": to_jalali_str(project.planned_start), "planned_end_jalali": to_jalali_str(project.planned_end), "actual_start_jalali": to_jalali_str(project.actual_start), "actual_end_jalali": to_jalali_str(project.actual_end), "status": project.status, "progress": round(project.progress or 0.0, 1), "member_count": member_count, "stats": task_stats or {"total": 0, "completed": 0, "in_progress": 0, "blocked": 0, "overdue": 0}, "created_at": _iso(project.created_at), "updated_at": _iso(project.updated_at), } def project_member_dict(member) -> dict: username = "" display_name = "" try: from sqlalchemy.inspection import inspect insp = inspect(member) if "user" in insp.attrs and insp.attrs.user.loaded and member.user: username = member.user.username display_name = member.user.display_name except Exception: pass return { "id": str(member.id), "project_id": str(member.project_id), "user_id": str(member.user_id), "username": username, "display_name": display_name, "role": member.role, "created_at": _iso(member.created_at), } def native_task_dict(task, project_name: str | None = None, children_count: int = 0) -> dict: assignee_name = task.assignee_name if not assignee_name: try: from sqlalchemy.inspection import inspect insp = inspect(task) if "assignee" in insp.attrs and "assignee" not in insp.unloaded and task.assignee: assignee_name = task.assignee.display_name except Exception: pass progress_logs_data = [] try: from sqlalchemy.inspection import inspect insp = inspect(task) if "progress_logs" in insp.attrs and "progress_logs" not in insp.unloaded and task.progress_logs: progress_logs_data = [ { "id": str(log.id), "progress_before": log.progress_before, "progress_after": log.progress_after, "human_resources_used": log.human_resources_used or [], "materials_used": log.materials_used or [], "description": log.description or "", "logged_by": log.logged_by or "", "created_at": _iso(log.created_at), } for log in task.progress_logs ] except Exception: pass return { "id": str(task.id), "project_id": str(task.project_id), "parent_id": str(task.parent_id) if task.parent_id else None, "title": task.title, "description": task.description or "", "status": task.status, "priority": task.priority, "assignee_id": str(task.assignee_id) if task.assignee_id else None, "assignee_name": assignee_name, "planned_start": _iso(task.planned_start), "planned_end": _iso(task.planned_end), "actual_start": _iso(task.actual_start), "actual_end": _iso(task.actual_end), "planned_start_jalali": to_jalali_str(task.planned_start), "planned_end_jalali": to_jalali_str(task.planned_end), "actual_start_jalali": to_jalali_str(task.actual_start), "actual_end_jalali": to_jalali_str(task.actual_end), "duration": task.duration or 1, "progress": task.progress or 0, "qc_status": task.qc_status or "none", "project_name": project_name, "human_resources": getattr(task, "human_resources", None) or [], "materials": getattr(task, "materials", None) or [], "progress_logs": progress_logs_data, "created_at": _iso(task.created_at), "updated_at": _iso(task.updated_at), } def task_relation_dict(rel, from_title: str = "", to_title: str = "") -> dict: from_title_val = from_title to_title_val = to_title try: from sqlalchemy.inspection import inspect insp = inspect(rel) if not from_title_val and "from_task" in insp.attrs and insp.attrs.from_task.loaded and rel.from_task: from_title_val = rel.from_task.title if not to_title_val and "to_task" in insp.attrs and insp.attrs.to_task.loaded and rel.to_task: to_title_val = rel.to_task.title except Exception: pass return { "id": str(rel.id), "project_id": str(rel.project_id), "from_task_id": str(rel.from_task_id), "to_task_id": str(rel.to_task_id), "from_task_title": from_title_val, "to_task_title": to_title_val, "relation_type": rel.relation_type, "lag_days": rel.lag_days, "created_at": _iso(rel.created_at), }