Coverage for modules\planner\api.py: 36%

78 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-05 09:25 +0800

1from ninja import Router 

2from typing import List, Optional 

3from uuid import UUID 

4from core.auth import JWTAuth 

5from core.permissions import require_tenant 

6from core.exceptions import NotFoundException 

7from .schemas import ( 

8 ProjectIn, ProjectOut, 

9 BoardOut, BoardColumnOut, 

10 TaskIn, TaskMoveIn, TaskUpdateIn, TaskOut, 

11 TaskCommentIn, TaskCommentOut, 

12 TimeLogIn, TimeLogOut 

13) 

14from .models import Project, Board, BoardColumn, Task, TaskComment, TimeLog 

15from .services import PlannerService 

16 

17router = Router(tags=["Planner & Projects"]) 

18 

19# ── Projects ────────────────────────────────────────────────────────────────── 

20@router.get("/projects", auth=JWTAuth(), response={200: List[ProjectOut]}) 

21def list_projects(request): 

22 tenant_id = require_tenant(request) 

23 projects = PlannerService.visible_projects(tenant_id, request.user).select_related('lead', 'department') 

24 result = [] 

25 for p in projects: 

26 total_tasks = p.tasks.count() 

27 completed_tasks = p.tasks.filter(status='DONE').count() 

28 result.append(ProjectOut( 

29 id=p.id, 

30 name=p.name, 

31 key=p.key, 

32 description=p.description, 

33 color=p.color, 

34 status=p.status, 

35 lead_id=p.lead.id if p.lead else None, 

36 lead_name=p.lead.full_name if p.lead else None, 

37 tasks_count=total_tasks, 

38 completed_tasks_count=completed_tasks, 

39 created_at=p.created_at, 

40 updated_at=p.updated_at, 

41 department_id=p.department_id, 

42 department_name=p.department.name if p.department else None, 

43 goal=p.goal, 

44 roadmap_start=p.roadmap_start, 

45 roadmap_end=p.roadmap_end, 

46 )) 

47 return result 

48 

49@router.post("/projects", auth=JWTAuth(), response={201: ProjectOut}) 

50def create_project(request, payload: ProjectIn): 

51 tenant_id = require_tenant(request) 

52 p = PlannerService.create_project(tenant_id, request.user, payload) 

53 return 201, ProjectOut( 

54 id=p.id, 

55 name=p.name, 

56 key=p.key, 

57 description=p.description, 

58 color=p.color, 

59 status=p.status, 

60 lead_id=p.lead.id if p.lead else None, 

61 lead_name=p.lead.full_name if p.lead else None, 

62 tasks_count=0, 

63 completed_tasks_count=0, 

64 created_at=p.created_at, 

65 updated_at=p.updated_at, 

66 department_id=p.department_id, 

67 department_name=p.department.name if p.department else None, 

68 goal=p.goal, 

69 roadmap_start=p.roadmap_start, 

70 roadmap_end=p.roadmap_end, 

71 ) 

72 

73# ── Boards ──────────────────────────────────────────────────────────────────── 

74@router.get("/projects/{project_id}/board", auth=JWTAuth(), response={200: BoardOut}) 

75def get_project_board(request, project_id: UUID): 

76 tenant_id = require_tenant(request) 

77 board = Board.objects.filter(project_id=project_id, tenant_id=tenant_id, project__in=PlannerService.visible_projects(tenant_id, request.user)).prefetch_related('columns').first() 

78 if not board: 

79 raise NotFoundException("Board not found for this project.") 

80 

81 return BoardOut( 

82 id=board.id, 

83 project_id=board.project_id, 

84 name=board.name, 

85 columns=[ 

86 BoardColumnOut( 

87 id=c.id, 

88 name=c.name, 

89 order=c.order, 

90 limit=c.limit, 

91 status_category=c.status_category, 

92 ) for c in board.columns.all() 

93 ] 

94 ) 

95 

96# ── Tasks ───────────────────────────────────────────────────────────────────── 

97@router.get("/tasks", auth=JWTAuth(), response={200: List[TaskOut]}) 

98def list_tasks(request, project_id: Optional[UUID] = None, assignee_id: Optional[UUID] = None, status: Optional[str] = None): 

99 tenant_id = require_tenant(request) 

100 qs = Task.objects.filter(tenant_id=tenant_id, project__in=PlannerService.visible_projects(tenant_id, request.user)).select_related('project', 'column', 'assignee', 'reporter') 

101 if project_id: 

102 qs = qs.filter(project_id=project_id) 

103 if assignee_id: 

104 qs = qs.filter(assignee_id=assignee_id) 

105 if status: 

106 qs = qs.filter(status=status) 

107 

108 result = [] 

109 for t in qs: 

110 result.append(TaskOut( 

111 id=t.id, 

112 project_id=t.project.id, 

113 project_key=t.project.key, 

114 board_id=t.board_id, 

115 column_id=t.column.id if t.column else None, 

116 column_name=t.column.name if t.column else None, 

117 title=t.title, 

118 description=t.description, 

119 task_number=t.task_number, 

120 task_code=t.task_code, 

121 priority=t.priority, 

122 status=t.status, 

123 order=t.order, 

124 assignee_id=t.assignee.id if t.assignee else None, 

125 assignee_name=t.assignee.full_name if t.assignee else None, 

126 assignee_avatar=t.assignee.avatar_url if t.assignee else None, 

127 reporter_id=t.reporter.id, 

128 reporter_name=t.reporter.full_name, 

129 due_date=t.due_date, 

130 estimated_hours=t.estimated_hours, 

131 logged_hours=t.logged_hours, 

132 tags=t.tags, 

133 comments_count=t.comments.count(), 

134 created_at=t.created_at, 

135 updated_at=t.updated_at, 

136 )) 

137 return result 

138 

139@router.post("/tasks", auth=JWTAuth(), response={201: TaskOut}) 

140def create_task(request, payload: TaskIn): 

141 tenant_id = require_tenant(request) 

142 t = PlannerService.create_task(tenant_id, request.user, payload) 

143 return 201, TaskOut( 

144 id=t.id, 

145 project_id=t.project.id, 

146 project_key=t.project.key, 

147 board_id=t.board_id, 

148 column_id=t.column.id if t.column else None, 

149 column_name=t.column.name if t.column else None, 

150 title=t.title, 

151 description=t.description, 

152 task_number=t.task_number, 

153 task_code=t.task_code, 

154 priority=t.priority, 

155 status=t.status, 

156 order=t.order, 

157 assignee_id=t.assignee.id if t.assignee else None, 

158 assignee_name=t.assignee.full_name if t.assignee else None, 

159 assignee_avatar=t.assignee.avatar_url if t.assignee else None, 

160 reporter_id=t.reporter.id, 

161 reporter_name=t.reporter.full_name, 

162 due_date=t.due_date, 

163 estimated_hours=t.estimated_hours, 

164 logged_hours=t.logged_hours, 

165 tags=t.tags, 

166 comments_count=0, 

167 created_at=t.created_at, 

168 updated_at=t.updated_at, 

169 ) 

170 

171@router.post("/tasks/{task_id}/move", auth=JWTAuth(), response={200: TaskOut}) 

172def move_task(request, task_id: UUID, payload: TaskMoveIn): 

173 tenant_id = require_tenant(request) 

174 t = PlannerService.move_task(tenant_id, request.user, task_id, payload.column_id, payload.order, payload.status) 

175 return TaskOut( 

176 id=t.id, 

177 project_id=t.project.id, 

178 project_key=t.project.key, 

179 board_id=t.board_id, 

180 column_id=t.column.id if t.column else None, 

181 column_name=t.column.name if t.column else None, 

182 title=t.title, 

183 description=t.description, 

184 task_number=t.task_number, 

185 task_code=t.task_code, 

186 priority=t.priority, 

187 status=t.status, 

188 order=t.order, 

189 assignee_id=t.assignee.id if t.assignee else None, 

190 assignee_name=t.assignee.full_name if t.assignee else None, 

191 assignee_avatar=t.assignee.avatar_url if t.assignee else None, 

192 reporter_id=t.reporter.id, 

193 reporter_name=t.reporter.full_name, 

194 due_date=t.due_date, 

195 estimated_hours=t.estimated_hours, 

196 logged_hours=t.logged_hours, 

197 tags=t.tags, 

198 comments_count=t.comments.count(), 

199 created_at=t.created_at, 

200 updated_at=t.updated_at, 

201 ) 

202 

203# ── Comments & Time Logs ────────────────────────────────────────────────────── 

204@router.get("/tasks/{task_id}/comments", auth=JWTAuth(), response={200: List[TaskCommentOut]}) 

205def list_comments(request, task_id: UUID): 

206 tenant_id = require_tenant(request) 

207 comments = TaskComment.objects.filter(task_id=task_id, tenant_id=tenant_id).select_related('author') 

208 return [ 

209 TaskCommentOut( 

210 id=c.id, 

211 task_id=c.task_id, 

212 author_id=c.author.id, 

213 author_name=c.author.full_name, 

214 author_avatar=c.author.avatar_url, 

215 content=c.content, 

216 created_at=c.created_at, 

217 ) for c in comments 

218 ] 

219 

220@router.post("/tasks/{task_id}/comments", auth=JWTAuth(), response={201: TaskCommentOut}) 

221def add_comment(request, task_id: UUID, payload: TaskCommentIn): 

222 tenant_id = require_tenant(request) 

223 task = Task.objects.filter(id=task_id, tenant_id=tenant_id, project__in=PlannerService.visible_projects(tenant_id, request.user)).first() 

224 if not task: 

225 raise NotFoundException("Task not found.") 

226 

227 c = TaskComment.objects.create( 

228 tenant_id=tenant_id, 

229 task=task, 

230 author=request.user, 

231 content=payload.content, 

232 ) 

233 return 201, TaskCommentOut( 

234 id=c.id, 

235 task_id=c.task_id, 

236 author_id=c.author.id, 

237 author_name=c.author.full_name, 

238 author_avatar=c.author.avatar_url, 

239 content=c.content, 

240 created_at=c.created_at, 

241 ) 

242 

243@router.post("/tasks/{task_id}/timelogs", auth=JWTAuth(), response={201: TimeLogOut}) 

244def log_time(request, task_id: UUID, payload: TimeLogIn): 

245 tenant_id = require_tenant(request) 

246 task = Task.objects.filter(id=task_id, tenant_id=tenant_id, project__in=PlannerService.visible_projects(tenant_id, request.user)).first() 

247 if not task: 

248 raise NotFoundException("Task not found.") 

249 

250 from django.utils import timezone 

251 tl = TimeLog.objects.create( 

252 tenant_id=tenant_id, 

253 task=task, 

254 user=request.user, 

255 hours=payload.hours, 

256 description=payload.description, 

257 logged_date=payload.logged_date or timezone.now().date(), 

258 ) 

259 return 201, TimeLogOut( 

260 id=tl.id, 

261 task_id=tl.task_id, 

262 user_id=tl.user.id, 

263 user_name=tl.user.full_name, 

264 hours=tl.hours, 

265 description=tl.description, 

266 logged_date=tl.logged_date, 

267 created_at=tl.created_at, 

268 )