mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 05:10:26 -05:00
b4486a627f
- 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
161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
"""
|
|
API v1 - Projects sub-blueprint.
|
|
Routes under /api/v1/projects.
|
|
"""
|
|
|
|
from flask import Blueprint, g, jsonify, request
|
|
from marshmallow import ValidationError
|
|
|
|
from app.utils.api_auth import require_api_token
|
|
from app.utils.api_responses import (
|
|
error_response,
|
|
forbidden_response,
|
|
handle_validation_error,
|
|
not_found_response,
|
|
validation_error_response,
|
|
)
|
|
|
|
api_v1_projects_bp = Blueprint("api_v1_projects", __name__, url_prefix="/api/v1")
|
|
|
|
|
|
@api_v1_projects_bp.route("/projects", methods=["GET"])
|
|
@require_api_token("read:projects")
|
|
def list_projects():
|
|
"""List all projects."""
|
|
from app.services import ProjectService
|
|
from app.utils.scope_filter import get_allowed_client_ids
|
|
|
|
status = request.args.get("status", "active")
|
|
client_id = request.args.get("client_id", type=int)
|
|
page = request.args.get("page", 1, type=int)
|
|
per_page = min(request.args.get("per_page", 50, type=int), 100)
|
|
scope_client_ids = get_allowed_client_ids(g.api_user)
|
|
|
|
project_service = ProjectService()
|
|
result = project_service.list_projects(
|
|
status=status,
|
|
client_id=client_id,
|
|
page=page,
|
|
per_page=per_page,
|
|
scope_client_ids=scope_client_ids,
|
|
)
|
|
pag = result["pagination"]
|
|
pagination_dict = {
|
|
"page": pag.page,
|
|
"per_page": pag.per_page,
|
|
"total": pag.total,
|
|
"pages": pag.pages,
|
|
"has_next": pag.has_next,
|
|
"has_prev": pag.has_prev,
|
|
"next_page": pag.page + 1 if pag.has_next else None,
|
|
"prev_page": pag.page - 1 if pag.has_prev else None,
|
|
}
|
|
return jsonify({"projects": [p.to_dict() for p in result["projects"]], "pagination": pagination_dict})
|
|
|
|
|
|
@api_v1_projects_bp.route("/projects/<int:project_id>", methods=["GET"])
|
|
@require_api_token("read:projects")
|
|
def get_project(project_id):
|
|
"""Get a specific project."""
|
|
from app.services import ProjectService
|
|
from app.utils.scope_filter import user_can_access_project
|
|
|
|
project_service = ProjectService()
|
|
result = project_service.get_project_with_details(project_id=project_id, include_time_entries=False)
|
|
|
|
if not result:
|
|
return not_found_response("Project", project_id)
|
|
if not user_can_access_project(g.api_user, project_id):
|
|
return forbidden_response("You do not have access to this project")
|
|
|
|
return jsonify({"project": result.to_dict()})
|
|
|
|
|
|
@api_v1_projects_bp.route("/projects", methods=["POST"])
|
|
@require_api_token("write:projects")
|
|
def create_project():
|
|
"""Create a new project."""
|
|
from app.schemas import ProjectCreateSchema
|
|
from app.services import ProjectService
|
|
|
|
data = request.get_json() or {}
|
|
if not data.get("name"):
|
|
return validation_error_response(
|
|
errors={"name": ["Project name is required"]},
|
|
message="Project name is required",
|
|
)
|
|
try:
|
|
loaded = ProjectCreateSchema(partial=True).load(data)
|
|
except ValidationError as err:
|
|
return handle_validation_error(err)
|
|
|
|
project_service = ProjectService()
|
|
result = project_service.create_project(
|
|
name=loaded["name"],
|
|
client_id=loaded.get("client_id"),
|
|
created_by=g.api_user.id,
|
|
description=loaded.get("description"),
|
|
billable=loaded.get("billable", True),
|
|
hourly_rate=loaded.get("hourly_rate"),
|
|
code=loaded.get("code"),
|
|
budget_amount=loaded.get("budget_amount"),
|
|
budget_threshold_percent=loaded.get("budget_threshold_percent"),
|
|
billing_ref=loaded.get("billing_ref"),
|
|
)
|
|
|
|
if not result.get("success"):
|
|
return error_response(result.get("message", "Could not create project"), status_code=400)
|
|
|
|
return jsonify({"message": "Project created successfully", "project": result["project"].to_dict()}), 201
|
|
|
|
|
|
@api_v1_projects_bp.route("/projects/<int:project_id>", methods=["PUT", "PATCH"])
|
|
@require_api_token("write:projects")
|
|
def update_project(project_id):
|
|
"""Update a project."""
|
|
from app.services import ProjectService
|
|
|
|
data = request.get_json() or {}
|
|
project_service = ProjectService()
|
|
update_kwargs = {}
|
|
if "name" in data:
|
|
update_kwargs["name"] = data["name"]
|
|
if "description" in data:
|
|
update_kwargs["description"] = data["description"]
|
|
if "client_id" in data:
|
|
update_kwargs["client_id"] = data["client_id"]
|
|
if "hourly_rate" in data:
|
|
update_kwargs["hourly_rate"] = data["hourly_rate"]
|
|
if "estimated_hours" in data:
|
|
update_kwargs["estimated_hours"] = data["estimated_hours"]
|
|
if "status" in data:
|
|
update_kwargs["status"] = data["status"]
|
|
if "code" in data:
|
|
update_kwargs["code"] = data["code"]
|
|
if "budget_amount" in data:
|
|
update_kwargs["budget_amount"] = data["budget_amount"]
|
|
if "billing_ref" in data:
|
|
update_kwargs["billing_ref"] = data["billing_ref"]
|
|
|
|
result = project_service.update_project(project_id=project_id, user_id=g.api_user.id, **update_kwargs)
|
|
|
|
if not result.get("success"):
|
|
return error_response(result.get("message", "Could not update project"), status_code=400)
|
|
|
|
return jsonify({"message": "Project updated successfully", "project": result["project"].to_dict()})
|
|
|
|
|
|
@api_v1_projects_bp.route("/projects/<int:project_id>", methods=["DELETE"])
|
|
@require_api_token("write:projects")
|
|
def delete_project(project_id):
|
|
"""Delete/archive a project."""
|
|
from app.services import ProjectService
|
|
|
|
project_service = ProjectService()
|
|
result = project_service.archive_project(project_id=project_id, user_id=g.api_user.id, reason="Archived via API")
|
|
|
|
if not result.get("success"):
|
|
return error_response(result.get("message", "Could not archive project"), status_code=404)
|
|
|
|
return jsonify({"message": "Project archived successfully"})
|