feat: enhance weekly report and realtime notifications

Weekly Report (fix-weekly-report):
- Remove 5-task limit, show all tasks per category
- Add blocked tasks with blocker_reason and blocked_since
- Add next week tasks (due in coming week)
- Add assignee_name, completed_at, days_overdue to task details
- Frontend collapsible sections for each task category
- 8 new tests for enhanced report content

Realtime Notifications (fix-realtime-notifications):
- SQLAlchemy event-based notification publishing
- Redis Pub/Sub for multi-process broadcast
- Fix soft rollback handler stacking issue
- Fix ping scheduling drift (send immediately when interval expires)
- Frontend NotificationContext with WebSocket reconnection

Spec Fixes:
- Add missing ## Purpose sections to 5 specs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-12-30 20:52:08 +08:00
parent 10db2c9d1f
commit 64874d5425
25 changed files with 1034 additions and 140 deletions

View File

@@ -1,7 +1,7 @@
import pytest
import uuid
from datetime import datetime, timedelta
from app.models import User, Space, Project, Task, TaskStatus, ScheduledReport, ReportHistory
from app.models import User, Space, Project, Task, TaskStatus, ScheduledReport, ReportHistory, Blocker
from app.services.report_service import ReportService
@@ -258,3 +258,186 @@ class TestReportAPI:
data = response.json()
assert data["id"] == report.id
assert "content" in data
class TestWeeklyReportContent:
"""Tests for enhanced weekly report content (blocked/next_week tasks)."""
def test_blocked_tasks_included(self, db, test_user, test_project, test_statuses):
"""Test that blocked tasks are included in weekly stats."""
# Create a task with blocker
blocked_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Blocked Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
assignee_id=test_user.id,
)
db.add(blocked_task)
db.flush()
# Create an unresolved blocker
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=blocked_task.id,
reported_by=test_user.id,
reason="Waiting for external dependency",
)
db.add(blocker)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["blocked_count"] == 1
assert len(stats["projects"]) == 1
assert stats["projects"][0]["blocked_count"] == 1
assert len(stats["projects"][0]["blocked_tasks"]) == 1
assert stats["projects"][0]["blocked_tasks"][0]["title"] == "Blocked Task"
assert stats["projects"][0]["blocked_tasks"][0]["blocker_reason"] == "Waiting for external dependency"
def test_resolved_blocker_not_included(self, db, test_user, test_project, test_statuses):
"""Test that resolved blockers are not counted."""
# Create a task with resolved blocker
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Previously Blocked Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
)
db.add(task)
db.flush()
# Create a resolved blocker
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=task.id,
reported_by=test_user.id,
reason="Was blocked",
resolved_by=test_user.id,
resolved_at=datetime.utcnow(),
resolution_note="Fixed",
)
db.add(blocker)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["blocked_count"] == 0
assert stats["projects"][0]["blocked_count"] == 0
def test_next_week_tasks_included(self, db, test_user, test_project, test_statuses):
"""Test that next week tasks are included in weekly stats."""
# Calculate next week dates
week_start = ReportService.get_week_start()
next_week_date = week_start + timedelta(days=10) # Next week
# Create a task due next week
next_week_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Next Week Task",
status_id=test_statuses["todo"].id,
due_date=next_week_date,
created_by=test_user.id,
assignee_id=test_user.id,
)
db.add(next_week_task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["next_week_count"] == 1
assert len(stats["projects"][0]["next_week_tasks"]) == 1
assert stats["projects"][0]["next_week_tasks"][0]["title"] == "Next Week Task"
def test_completed_task_not_in_next_week(self, db, test_user, test_project, test_statuses):
"""Test that completed tasks are not included in next week list."""
week_start = ReportService.get_week_start()
next_week_date = week_start + timedelta(days=10)
# Create a completed task due next week
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Done Next Week Task",
status_id=test_statuses["done"].id,
due_date=next_week_date,
created_by=test_user.id,
)
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["next_week_count"] == 0
def test_task_details_include_assignee_name(self, db, test_user, test_project, test_statuses):
"""Test that task details include assignee name."""
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Assigned Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
assignee_id=test_user.id,
)
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert len(stats["projects"][0]["in_progress_tasks"]) == 1
assert stats["projects"][0]["in_progress_tasks"][0]["assignee_name"] == "Report User"
def test_overdue_days_calculated(self, db, test_user, test_project, test_statuses):
"""Test that days_overdue is correctly calculated."""
# Create task overdue by 5 days
overdue_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="5 Days Overdue",
status_id=test_statuses["todo"].id,
due_date=datetime.utcnow() - timedelta(days=5),
created_by=test_user.id,
)
db.add(overdue_task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert len(stats["projects"][0]["overdue_tasks"]) == 1
assert stats["projects"][0]["overdue_tasks"][0]["days_overdue"] >= 5
def test_full_task_lists_no_limit(self, db, test_user, test_project, test_statuses):
"""Test that task lists have no 5-item limit."""
# Create 10 completed tasks
for i in range(10):
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title=f"Completed Task {i}",
status_id=test_statuses["done"].id,
created_by=test_user.id,
)
task.updated_at = datetime.utcnow()
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["completed_count"] == 10
assert len(stats["projects"][0]["completed_tasks"]) == 10 # No limit
def test_summary_includes_all_counts(self, db, test_user, test_project, test_statuses):
"""Test that summary includes all new count fields."""
stats = ReportService.get_weekly_stats(db, test_user.id)
summary = stats["summary"]
assert "completed_count" in summary
assert "in_progress_count" in summary
assert "overdue_count" in summary
assert "blocked_count" in summary
assert "next_week_count" in summary
assert "total_tasks" in summary