feat(reports): support multi-user chart series
This commit is contained in:
@@ -387,31 +387,57 @@ def build_chart_report(actor, raw_filters) -> dict:
|
|||||||
filters = load_report_filters(actor, raw_filters)
|
filters = load_report_filters(actor, raw_filters)
|
||||||
entries = list(_base_queryset(filters))
|
entries = list(_base_queryset(filters))
|
||||||
summary = _summary_from_entries(entries)
|
summary = _summary_from_entries(entries)
|
||||||
buckets: dict[str, dict] = {}
|
grouped_entries: dict[str | None, list[TimeEntry]] = defaultdict(list)
|
||||||
|
if filters.is_workspace_scope and not filters.user_id:
|
||||||
|
for entry in entries:
|
||||||
|
grouped_entries[str(entry.user_id)].append(entry)
|
||||||
|
else:
|
||||||
|
grouped_entries[filters.user_id] = entries
|
||||||
|
|
||||||
for entry in entries:
|
serialized_series = []
|
||||||
local_start = _localize_datetime(entry.start_time)
|
for _, series_entries in sorted(
|
||||||
bucket_id, bucket_date = _bucket_key(filters, local_start)
|
grouped_entries.items(),
|
||||||
bucket = buckets.setdefault(
|
key=lambda item: _user_display(item[1][0].user).lower() if item[1] else "",
|
||||||
bucket_id,
|
):
|
||||||
|
if not series_entries:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buckets: dict[str, dict] = {}
|
||||||
|
for entry in series_entries:
|
||||||
|
local_start = _localize_datetime(entry.start_time)
|
||||||
|
bucket_id, bucket_date = _bucket_key(filters, local_start)
|
||||||
|
bucket = buckets.setdefault(
|
||||||
|
bucket_id,
|
||||||
|
{
|
||||||
|
"bucket_key": bucket_id,
|
||||||
|
"bucket_label": _bucket_label(filters, bucket_date),
|
||||||
|
"total_seconds": 0,
|
||||||
|
"total_duration": "00:00:00",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bucket["total_seconds"] += get_entry_duration_seconds(entry)
|
||||||
|
|
||||||
|
serialized_buckets = []
|
||||||
|
for bucket in sorted(buckets.values(), key=lambda item: item["bucket_key"]):
|
||||||
|
bucket["total_duration"] = _format_duration_seconds(bucket["total_seconds"])
|
||||||
|
serialized_buckets.append(bucket)
|
||||||
|
|
||||||
|
user = series_entries[0].user
|
||||||
|
serialized_series.append(
|
||||||
{
|
{
|
||||||
"bucket_key": bucket_id,
|
"user": {
|
||||||
"bucket_label": _bucket_label(filters, bucket_date),
|
"id": str(user.id),
|
||||||
"total_seconds": 0,
|
"name": _user_display(user),
|
||||||
"total_duration": "00:00:00",
|
"mobile": user.mobile,
|
||||||
},
|
},
|
||||||
|
"buckets": serialized_buckets,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
bucket["total_seconds"] += get_entry_duration_seconds(entry)
|
|
||||||
|
|
||||||
serialized_buckets = []
|
|
||||||
for bucket in sorted(buckets.values(), key=lambda item: item["bucket_key"]):
|
|
||||||
bucket["total_duration"] = _format_duration_seconds(bucket["total_seconds"])
|
|
||||||
serialized_buckets.append(bucket)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"scope": _scope_payload(filters),
|
"scope": _scope_payload(filters),
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
"buckets": serialized_buckets,
|
"series": serialized_series,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,28 @@ class ReportViewTests(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.data["summary"]["total_duration"], "01:00:00")
|
self.assertEqual(response.data["summary"]["total_duration"], "01:00:00")
|
||||||
|
self.assertEqual(len(response.data["series"]), 1)
|
||||||
|
self.assertEqual(response.data["series"][0]["user"]["id"], str(self.member.id))
|
||||||
|
|
||||||
|
def test_admin_chart_without_user_filter_returns_series_for_all_users(self):
|
||||||
|
self.client.force_authenticate(user=self.admin)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"apps.reports.services.aggregation.timezone.localdate",
|
||||||
|
return_value=date(2026, 4, 20),
|
||||||
|
):
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/reports/chart/",
|
||||||
|
{"workspace": str(self.workspace.id), "period": "this_month"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["summary"]["total_duration"], "03:00:00")
|
||||||
|
self.assertEqual(len(response.data["series"]), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
{series["user"]["id"] for series in response.data["series"]},
|
||||||
|
{str(self.owner.id), str(self.member.id)},
|
||||||
|
)
|
||||||
|
|
||||||
def test_admin_can_request_combined_table_report(self):
|
def test_admin_can_request_combined_table_report(self):
|
||||||
self.client.force_authenticate(user=self.admin)
|
self.client.force_authenticate(user=self.admin)
|
||||||
|
|||||||
Reference in New Issue
Block a user