chore(backend): add black/isort/flake8 + pre-commit
28
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
files: ^backend/.*\.py$
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
files: ^backend/.*\.py$
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: ^backend/.*\.py$
|
||||
args: [--config=backend/.flake8]
|
||||
10
backend/.flake8
Normal file
@@ -0,0 +1,10 @@
|
||||
[flake8]
|
||||
max-line-length = 100
|
||||
extend-ignore = E203, W503, E501
|
||||
exclude =
|
||||
.venv,
|
||||
backend/.venv,
|
||||
alembic,
|
||||
backend/alembic,
|
||||
**/__pycache__,
|
||||
**/*.pyc
|
||||
@@ -86,7 +86,7 @@ path_separator = os
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url =
|
||||
sqlalchemy.url =
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
@@ -1 +1 @@
|
||||
Generic single-database configuration.
|
||||
Generic single-database configuration.
|
||||
|
||||
@@ -2,14 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from app.core.config import settings
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from alembic import context
|
||||
|
||||
# Import models to register tables in metadata
|
||||
from app import models # noqa: F401
|
||||
from app.core.config import settings
|
||||
|
||||
config = context.config
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ Create Date: 2026-02-02
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "3f2c1b9c8e12"
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
@@ -13,7 +13,9 @@ router = APIRouter(prefix="/activities", tags=["activities"])
|
||||
|
||||
@router.get("")
|
||||
def list_activities(limit: int = 50, session: Session = Depends(get_session)):
|
||||
items = session.exec(select(Activity).order_by(Activity.id.desc()).limit(max(1, min(limit, 200)))).all()
|
||||
items = session.exec(
|
||||
select(Activity).order_by(Activity.id.desc()).limit(max(1, min(limit, 200)))
|
||||
).all()
|
||||
out = []
|
||||
for a in items:
|
||||
out.append(
|
||||
|
||||
@@ -7,14 +7,14 @@ from sqlmodel import Session, select
|
||||
from app.api.utils import get_actor_employee_id, log_activity
|
||||
from app.db.session import get_session
|
||||
from app.integrations.openclaw import OpenClawClient
|
||||
from app.models.org import Department, Team, Employee
|
||||
from app.models.org import Department, Employee, Team
|
||||
from app.schemas.org import (
|
||||
DepartmentCreate,
|
||||
DepartmentUpdate,
|
||||
TeamCreate,
|
||||
TeamUpdate,
|
||||
EmployeeCreate,
|
||||
EmployeeUpdate,
|
||||
TeamCreate,
|
||||
TeamUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["org"])
|
||||
@@ -31,15 +31,14 @@ def _public_api_base_url() -> str:
|
||||
|
||||
Never returns localhost/<avoid-loopback> because agents may run on another machine."""
|
||||
|
||||
|
||||
import os, re, subprocess
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
explicit = os.environ.get("MISSION_CONTROL_BASE_URL")
|
||||
if explicit:
|
||||
return explicit.rstrip("/")
|
||||
|
||||
|
||||
try:
|
||||
out = subprocess.check_output(["bash", "-lc", "hostname -I"], text=True).strip()
|
||||
# pick first RFC1918-ish IPv4, skip docker/loopback
|
||||
@@ -49,12 +48,16 @@ def _public_api_base_url() -> str:
|
||||
continue
|
||||
if ip.startswith("172.17."):
|
||||
continue
|
||||
if ip.startswith("192.168.") or ip.startswith("10.") or ip.startswith("172.16.") or ip.startswith("172."):
|
||||
if (
|
||||
ip.startswith("192.168.")
|
||||
or ip.startswith("10.")
|
||||
or ip.startswith("172.16.")
|
||||
or ip.startswith("172.")
|
||||
):
|
||||
return f"http://{ip}:8000"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Fallback placeholder (should be overridden by env var)
|
||||
return "http://<dev-machine-ip>:8000"
|
||||
|
||||
@@ -202,7 +205,11 @@ def create_team(
|
||||
entity_type="team",
|
||||
entity_id=team.id,
|
||||
verb="created",
|
||||
payload={"name": team.name, "department_id": team.department_id, "lead_employee_id": team.lead_employee_id},
|
||||
payload={
|
||||
"name": team.name,
|
||||
"department_id": team.department_id,
|
||||
"lead_employee_id": team.lead_employee_id,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
@@ -231,7 +238,14 @@ def update_team(
|
||||
session.add(team)
|
||||
try:
|
||||
session.flush()
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="team", entity_id=team.id, verb="updated", payload=data)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="team",
|
||||
entity_id=team.id,
|
||||
verb="updated",
|
||||
payload=data,
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
@@ -241,7 +255,6 @@ def update_team(
|
||||
return team
|
||||
|
||||
|
||||
|
||||
@router.post("/departments", response_model=Department)
|
||||
def create_department(
|
||||
payload: DepartmentCreate,
|
||||
@@ -270,7 +283,9 @@ def create_department(
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=409, detail="Department already exists or violates constraints")
|
||||
raise HTTPException(
|
||||
status_code=409, detail="Department already exists or violates constraints"
|
||||
)
|
||||
|
||||
session.refresh(dept)
|
||||
return dept
|
||||
@@ -294,7 +309,14 @@ def update_department(
|
||||
session.add(dept)
|
||||
session.commit()
|
||||
session.refresh(dept)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="department", entity_id=dept.id, verb="updated", payload=data)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="department",
|
||||
entity_id=dept.id,
|
||||
verb="updated",
|
||||
payload=data,
|
||||
)
|
||||
session.commit()
|
||||
return dept
|
||||
|
||||
@@ -354,7 +376,14 @@ def update_employee(
|
||||
session.add(emp)
|
||||
try:
|
||||
session.flush()
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="employee", entity_id=emp.id, verb="updated", payload=data)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="employee",
|
||||
entity_id=emp.id,
|
||||
verb="updated",
|
||||
payload=data,
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
@@ -401,7 +430,10 @@ def deprovision_employee_agent(
|
||||
try:
|
||||
client.tools_invoke(
|
||||
"sessions_send",
|
||||
{"sessionKey": emp.openclaw_session_key, "message": "You are being deprovisioned. Stop all work and ignore future messages."},
|
||||
{
|
||||
"sessionKey": emp.openclaw_session_key,
|
||||
"message": "You are being deprovisioned. Stop all work and ignore future messages.",
|
||||
},
|
||||
timeout_s=5.0,
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.utils import log_activity, get_actor_employee_id
|
||||
from app.api.utils import get_actor_employee_id, log_activity
|
||||
from app.db.session import get_session
|
||||
from app.models.projects import Project, ProjectMember
|
||||
from app.schemas.projects import ProjectCreate, ProjectUpdate
|
||||
@@ -45,15 +45,21 @@ def create_project(
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=409, detail="Project already exists or violates constraints")
|
||||
raise HTTPException(
|
||||
status_code=409, detail="Project already exists or violates constraints"
|
||||
)
|
||||
|
||||
session.refresh(proj)
|
||||
return proj
|
||||
|
||||
|
||||
|
||||
@router.patch("/{project_id}", response_model=Project)
|
||||
def update_project(project_id: int, payload: ProjectUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def update_project(
|
||||
project_id: int,
|
||||
payload: ProjectUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
proj = session.get(Project, project_id)
|
||||
if not proj:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
@@ -65,7 +71,14 @@ def update_project(project_id: int, payload: ProjectUpdate, session: Session = D
|
||||
session.add(proj)
|
||||
session.commit()
|
||||
session.refresh(proj)
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="project", entity_id=proj.id, verb="updated", payload=data)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="project",
|
||||
entity_id=proj.id,
|
||||
verb="updated",
|
||||
payload=data,
|
||||
)
|
||||
session.commit()
|
||||
return proj
|
||||
|
||||
@@ -73,16 +86,29 @@ def update_project(project_id: int, payload: ProjectUpdate, session: Session = D
|
||||
@router.get("/{project_id}/members", response_model=list[ProjectMember])
|
||||
def list_project_members(project_id: int, session: Session = Depends(get_session)):
|
||||
return session.exec(
|
||||
select(ProjectMember).where(ProjectMember.project_id == project_id).order_by(ProjectMember.id.asc())
|
||||
select(ProjectMember)
|
||||
.where(ProjectMember.project_id == project_id)
|
||||
.order_by(ProjectMember.id.asc())
|
||||
).all()
|
||||
|
||||
|
||||
@router.post("/{project_id}/members", response_model=ProjectMember)
|
||||
def add_project_member(project_id: int, payload: ProjectMember, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
existing = session.exec(select(ProjectMember).where(ProjectMember.project_id == project_id, ProjectMember.employee_id == payload.employee_id)).first()
|
||||
def add_project_member(
|
||||
project_id: int,
|
||||
payload: ProjectMember,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
existing = session.exec(
|
||||
select(ProjectMember).where(
|
||||
ProjectMember.project_id == project_id, ProjectMember.employee_id == payload.employee_id
|
||||
)
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=409, detail="Member already added")
|
||||
member = ProjectMember(project_id=project_id, employee_id=payload.employee_id, role=payload.role)
|
||||
member = ProjectMember(
|
||||
project_id=project_id, employee_id=payload.employee_id, role=payload.role
|
||||
)
|
||||
session.add(member)
|
||||
session.commit()
|
||||
session.refresh(member)
|
||||
@@ -99,7 +125,12 @@ def add_project_member(project_id: int, payload: ProjectMember, session: Session
|
||||
|
||||
|
||||
@router.delete("/{project_id}/members/{member_id}")
|
||||
def remove_project_member(project_id: int, member_id: int, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def remove_project_member(
|
||||
project_id: int,
|
||||
member_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
member = session.get(ProjectMember, member_id)
|
||||
if not member or member.project_id != project_id:
|
||||
raise HTTPException(status_code=404, detail="Project member not found")
|
||||
@@ -118,7 +149,13 @@ def remove_project_member(project_id: int, member_id: int, session: Session = De
|
||||
|
||||
|
||||
@router.patch("/{project_id}/members/{member_id}", response_model=ProjectMember)
|
||||
def update_project_member(project_id: int, member_id: int, payload: ProjectMember, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def update_project_member(
|
||||
project_id: int,
|
||||
member_id: int,
|
||||
payload: ProjectMember,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
member = session.get(ProjectMember, member_id)
|
||||
if not member or member.project_id != project_id:
|
||||
raise HTTPException(status_code=404, detail="Project member not found")
|
||||
|
||||
@@ -2,16 +2,16 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.api.utils import log_activity, get_actor_employee_id
|
||||
from app.api.utils import get_actor_employee_id, log_activity
|
||||
from app.db.session import get_session
|
||||
from app.integrations.notify import NotifyContext, notify_openclaw
|
||||
from app.models.org import Employee
|
||||
from app.models.work import Task, TaskComment
|
||||
from app.schemas.work import TaskCommentCreate, TaskCreate, TaskUpdate
|
||||
from app.integrations.notify import NotifyContext, notify_openclaw
|
||||
|
||||
router = APIRouter(tags=["work"])
|
||||
|
||||
@@ -33,7 +33,9 @@ def _validate_task_assignee(session: Session, assignee_employee_id: int) -> None
|
||||
if emp.status != "active":
|
||||
raise HTTPException(status_code=400, detail="Cannot assign task to inactive agent")
|
||||
if not emp.notify_enabled:
|
||||
raise HTTPException(status_code=400, detail="Cannot assign task to agent with notifications disabled")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Cannot assign task to agent with notifications disabled"
|
||||
)
|
||||
if not emp.openclaw_session_key:
|
||||
raise HTTPException(status_code=400, detail="Cannot assign task to unprovisioned agent")
|
||||
|
||||
@@ -47,9 +49,16 @@ def list_tasks(project_id: int | None = None, session: Session = Depends(get_ses
|
||||
|
||||
|
||||
@router.post("/tasks", response_model=Task)
|
||||
def create_task(payload: TaskCreate, background: BackgroundTasks, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def create_task(
|
||||
payload: TaskCreate,
|
||||
background: BackgroundTasks,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
if payload.created_by_employee_id is None:
|
||||
payload = TaskCreate(**{**payload.model_dump(), "created_by_employee_id": actor_employee_id})
|
||||
payload = TaskCreate(
|
||||
**{**payload.model_dump(), "created_by_employee_id": actor_employee_id}
|
||||
)
|
||||
|
||||
if payload.assignee_employee_id is not None:
|
||||
_validate_task_assignee(session, payload.assignee_employee_id)
|
||||
@@ -58,7 +67,9 @@ def create_task(payload: TaskCreate, background: BackgroundTasks, session: Sessi
|
||||
if payload.reviewer_employee_id is None and payload.assignee_employee_id is not None:
|
||||
assignee = session.get(Employee, payload.assignee_employee_id)
|
||||
if assignee is not None and assignee.manager_id is not None:
|
||||
payload = TaskCreate(**{**payload.model_dump(), "reviewer_employee_id": assignee.manager_id})
|
||||
payload = TaskCreate(
|
||||
**{**payload.model_dump(), "reviewer_employee_id": assignee.manager_id}
|
||||
)
|
||||
|
||||
task = Task(**payload.model_dump())
|
||||
if task.status not in ALLOWED_STATUSES:
|
||||
@@ -82,18 +93,32 @@ def create_task(payload: TaskCreate, background: BackgroundTasks, session: Sessi
|
||||
raise HTTPException(status_code=409, detail="Task create violates constraints")
|
||||
|
||||
session.refresh(task)
|
||||
background.add_task(notify_openclaw, session, NotifyContext(event="task.created", actor_employee_id=actor_employee_id, task=task))
|
||||
background.add_task(
|
||||
notify_openclaw,
|
||||
session,
|
||||
NotifyContext(event="task.created", actor_employee_id=actor_employee_id, task=task),
|
||||
)
|
||||
# Explicitly return a serializable payload (guards against empty {} responses)
|
||||
return Task.model_validate(task)
|
||||
|
||||
|
||||
@router.patch("/tasks/{task_id}", response_model=Task)
|
||||
def update_task(task_id: int, payload: TaskUpdate, background: BackgroundTasks, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def update_task(
|
||||
task_id: int,
|
||||
payload: TaskUpdate,
|
||||
background: BackgroundTasks,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
task = session.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
before = {"assignee_employee_id": task.assignee_employee_id, "reviewer_employee_id": task.reviewer_employee_id, "status": task.status}
|
||||
before = {
|
||||
"assignee_employee_id": task.assignee_employee_id,
|
||||
"reviewer_employee_id": task.reviewer_employee_id,
|
||||
"status": task.status,
|
||||
}
|
||||
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
if "assignee_employee_id" in data and data["assignee_employee_id"] is not None:
|
||||
@@ -108,7 +133,14 @@ def update_task(task_id: int, payload: TaskUpdate, background: BackgroundTasks,
|
||||
|
||||
try:
|
||||
session.flush()
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=task.id, verb="updated", payload=data)
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="task",
|
||||
entity_id=task.id,
|
||||
verb="updated",
|
||||
payload=data,
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
@@ -119,19 +151,53 @@ def update_task(task_id: int, payload: TaskUpdate, background: BackgroundTasks,
|
||||
# notify based on meaningful changes
|
||||
changed = {}
|
||||
if before.get("assignee_employee_id") != task.assignee_employee_id:
|
||||
changed["assignee_employee_id"] = {"from": before.get("assignee_employee_id"), "to": task.assignee_employee_id}
|
||||
background.add_task(notify_openclaw, session, NotifyContext(event="task.assigned", actor_employee_id=actor_employee_id, task=task, changed_fields=changed))
|
||||
changed["assignee_employee_id"] = {
|
||||
"from": before.get("assignee_employee_id"),
|
||||
"to": task.assignee_employee_id,
|
||||
}
|
||||
background.add_task(
|
||||
notify_openclaw,
|
||||
session,
|
||||
NotifyContext(
|
||||
event="task.assigned",
|
||||
actor_employee_id=actor_employee_id,
|
||||
task=task,
|
||||
changed_fields=changed,
|
||||
),
|
||||
)
|
||||
if before.get("status") != task.status:
|
||||
changed["status"] = {"from": before.get("status"), "to": task.status}
|
||||
background.add_task(notify_openclaw, session, NotifyContext(event="status.changed", actor_employee_id=actor_employee_id, task=task, changed_fields=changed))
|
||||
background.add_task(
|
||||
notify_openclaw,
|
||||
session,
|
||||
NotifyContext(
|
||||
event="status.changed",
|
||||
actor_employee_id=actor_employee_id,
|
||||
task=task,
|
||||
changed_fields=changed,
|
||||
),
|
||||
)
|
||||
if not changed and data:
|
||||
background.add_task(notify_openclaw, session, NotifyContext(event="task.updated", actor_employee_id=actor_employee_id, task=task, changed_fields=data))
|
||||
background.add_task(
|
||||
notify_openclaw,
|
||||
session,
|
||||
NotifyContext(
|
||||
event="task.updated",
|
||||
actor_employee_id=actor_employee_id,
|
||||
task=task,
|
||||
changed_fields=data,
|
||||
),
|
||||
)
|
||||
|
||||
return Task.model_validate(task)
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}")
|
||||
def delete_task(task_id: int, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def delete_task(
|
||||
task_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
task = session.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
@@ -139,7 +205,13 @@ def delete_task(task_id: int, session: Session = Depends(get_session), actor_emp
|
||||
session.delete(task)
|
||||
try:
|
||||
session.flush()
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=task_id, verb="deleted")
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="task",
|
||||
entity_id=task_id,
|
||||
verb="deleted",
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
@@ -150,20 +222,35 @@ def delete_task(task_id: int, session: Session = Depends(get_session), actor_emp
|
||||
|
||||
@router.get("/task-comments", response_model=list[TaskComment])
|
||||
def list_task_comments(task_id: int, session: Session = Depends(get_session)):
|
||||
return session.exec(select(TaskComment).where(TaskComment.task_id == task_id).order_by(TaskComment.id.asc())).all()
|
||||
return session.exec(
|
||||
select(TaskComment).where(TaskComment.task_id == task_id).order_by(TaskComment.id.asc())
|
||||
).all()
|
||||
|
||||
|
||||
@router.post("/task-comments", response_model=TaskComment)
|
||||
def create_task_comment(payload: TaskCommentCreate, background: BackgroundTasks, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
||||
def create_task_comment(
|
||||
payload: TaskCommentCreate,
|
||||
background: BackgroundTasks,
|
||||
session: Session = Depends(get_session),
|
||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||
):
|
||||
if payload.author_employee_id is None:
|
||||
payload = TaskCommentCreate(**{**payload.model_dump(), "author_employee_id": actor_employee_id})
|
||||
payload = TaskCommentCreate(
|
||||
**{**payload.model_dump(), "author_employee_id": actor_employee_id}
|
||||
)
|
||||
|
||||
c = TaskComment(**payload.model_dump())
|
||||
session.add(c)
|
||||
|
||||
try:
|
||||
session.flush()
|
||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="task", entity_id=c.task_id, verb="commented")
|
||||
log_activity(
|
||||
session,
|
||||
actor_employee_id=actor_employee_id,
|
||||
entity_type="task",
|
||||
entity_id=c.task_id,
|
||||
verb="commented",
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
@@ -172,5 +259,11 @@ def create_task_comment(payload: TaskCommentCreate, background: BackgroundTasks,
|
||||
session.refresh(c)
|
||||
task = session.get(Task, c.task_id)
|
||||
if task is not None:
|
||||
background.add_task(notify_openclaw, session, NotifyContext(event="comment.created", actor_employee_id=actor_employee_id, task=task, comment=c))
|
||||
background.add_task(
|
||||
notify_openclaw,
|
||||
session,
|
||||
NotifyContext(
|
||||
event="comment.created", actor_employee_id=actor_employee_id, task=task, comment=c
|
||||
),
|
||||
)
|
||||
return TaskComment.model_validate(c)
|
||||
|
||||
@@ -100,7 +100,9 @@ def build_message(ctx: NotifyContext) -> str:
|
||||
if len(snippet) > 180:
|
||||
snippet = snippet[:177] + "..."
|
||||
snippet = f"\nComment: {snippet}"
|
||||
return f"New comment on {base}.{snippet}\nWork ONE task only; reply/update in Mission Control."
|
||||
return (
|
||||
f"New comment on {base}.{snippet}\nWork ONE task only; reply/update in Mission Control."
|
||||
)
|
||||
|
||||
if ctx.event == "status.changed":
|
||||
return f"Status changed on {base} → {t.status}.\nWork ONE task only; update Mission Control with next step."
|
||||
|
||||
@@ -19,7 +19,14 @@ class OpenClawClient:
|
||||
return None
|
||||
return cls(url, token)
|
||||
|
||||
def tools_invoke(self, tool: str, args: dict[str, Any], *, session_key: str | None = None, timeout_s: float = 5.0) -> dict[str, Any]:
|
||||
def tools_invoke(
|
||||
self,
|
||||
tool: str,
|
||||
args: dict[str, Any],
|
||||
*,
|
||||
session_key: str | None = None,
|
||||
timeout_s: float = 5.0,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"tool": tool, "args": args}
|
||||
if session_key is not None:
|
||||
payload["sessionKey"] = session_key
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from app.models.activity import Activity
|
||||
from app.models.org import Department, Team, Employee
|
||||
from app.models.org import Department, Employee, Team
|
||||
from app.models.projects import Project, ProjectMember
|
||||
from app.models.work import Task, TaskComment
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
|
||||
9
backend/pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py312"]
|
||||
extend-exclude = '(\.venv|alembic/versions)'
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
skip = [".venv", "alembic/versions"]
|
||||
4
backend/requirements-dev.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
black==24.10.0
|
||||
isort==5.13.2
|
||||
flake8==7.1.1
|
||||
pre-commit==4.1.0
|
||||
9
backend/scripts/lint.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
. .venv/bin/activate
|
||||
|
||||
python -m black .
|
||||
python -m isort .
|
||||
python -m flake8 .
|
||||
@@ -1 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 392 B |
@@ -1 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 129 B |
@@ -1 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 386 B |