Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,32 @@ def _annotate_project_schedule_fields(projects: list[dict[str, Any]]) -> None:
project["start_status_text"] = start_status_text


def get_project_schedule_variance_days(project: dict[str, Any]) -> int | None:
target_date = parse_iso_date(project.get("targetDate"))
completed_date = parse_iso_date(project.get("completedAt"))
if target_date is None or completed_date is None:
return None
return (completed_date - target_date).days


def format_average_project_schedule_variance(
average_variance_days: float | None,
) -> str | None:
if average_variance_days is None:
return None
if abs(average_variance_days) < 0.05:
return "on time"

magnitude = abs(average_variance_days)
if magnitude.is_integer():
display = str(int(magnitude))
else:
display = f"{magnitude:.1f}".rstrip("0").rstrip(".")

direction = "late" if average_variance_days > 0 else "early"
return f"{display}d {direction}"


app = Flask(__name__)


Expand Down Expand Up @@ -1504,6 +1530,20 @@ def normalize_display_name(value: str | None) -> str:
for project in led_projects
if not project.get("is_inactive")
)
lead_completed_project_variances = [
variance_days
for project in led_projects
if is_completed_project(project)
for variance_days in [get_project_schedule_variance_days(project)]
if variance_days is not None
]
if lead_completed_project_variances:
average_completed_project_variance = sum(
lead_completed_project_variances
) / len(lead_completed_project_variances)
else:
average_completed_project_variance = None

project_names = {
proj.get("name") for proj in cycle_projects if proj.get("name")
}
Expand Down Expand Up @@ -1556,6 +1596,9 @@ def normalize_display_name(value: str | None) -> str:
"lead_completed_projects": lead_completed_projects,
"lead_current_projects": lead_current_projects,
"lead_incomplete_projects": lead_incomplete_projects,
"lead_completed_projects_avg_early_late": format_average_project_schedule_variance(
average_completed_project_variance
),
"platform_labels": platform_labels,
"platform_values": platform_values,
}
Expand Down
10 changes: 10 additions & 0 deletions templates/partials/person_content.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ <h1>{{ lead_completed_projects }}</h1>
<h1>{{ lead_incomplete_projects }}</h1>
</article>
</div>
<div>
<article>
<header><a href="https://linear.app/differential/projects/all">Avg Days Early/Late</a></header>
{% if lead_completed_projects_avg_early_late %}
<h1>{{ lead_completed_projects_avg_early_late }}</h1>
{% else %}
<h1>n/a</h1>
{% endif %}
</article>
</div>
</div>
{% if work_by_platform %}
<hr />
Expand Down
59 changes: 59 additions & 0 deletions tests/test_failing_dags.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,65 @@ def test_released_project_is_not_counted_as_current(self):
self.assertEqual(context["lead_current_projects"], 0)
self.assertEqual(context["lead_completed_projects"], 1)
self.assertEqual(context["lead_incomplete_projects"], 0)
self.assertEqual(context["lead_completed_projects_avg_early_late"], "on time")

def test_completed_project_average_shows_mixed_early_and_late_timing(self):
config = {
"people": {
"darryl": {
"team": "engineering",
"linear_username": "Darryl",
}
}
}
completed_projects = [
{
"id": "proj-1",
"name": "Launch Web Revamp",
"url": "https://linear.example/project/web-revamp",
"health": "onTrack",
"status": {"name": "Released"},
"completedAt": "2025-11-08T00:00:00.000Z",
"startDate": "2025-11-01",
"targetDate": "2025-11-10",
"lead": {"displayName": "Darryl"},
"initiatives": {"nodes": [{"id": "init-1", "name": "Cycle"}]},
"members": [],
},
{
"id": "proj-2",
"name": "Android Polish",
"url": "https://linear.example/project/android-polish",
"health": "onTrack",
"status": {"name": "Completed"},
"completedAt": "2025-11-16T00:00:00.000Z",
"startDate": "2025-11-01",
"targetDate": "2025-11-12",
"lead": {"displayName": "Darryl"},
"initiatives": {"nodes": [{"id": "init-1", "name": "Cycle"}]},
"members": [],
},
]

with patch.object(app_module, "load_config", return_value=config):
with patch.object(app_module, "get_open_issues_for_person", return_value=[]):
with patch.object(
app_module,
"get_completed_issues_for_person",
return_value=[],
):
with patch.object(app_module, "by_project", return_value={}):
with patch.object(app_module, "by_platform", return_value={}):
with patch.object(
app_module,
"get_projects",
return_value=completed_projects,
):
with patch.object(app_module, "get_support_slugs", return_value=[]):
context = app_module._build_person_context("darryl", 7, 1)

self.assertEqual(context["lead_completed_projects"], 2)
self.assertEqual(context["lead_completed_projects_avg_early_late"], "1d late")

def test_project_deadline_uses_hours_when_less_than_day_left(self):
config = {
Expand Down
Loading