Skip to content

Commit 19a4306

Browse files
authored
Merge pull request #245 from buildingSMART/IVS-658_Minor_Django_Admin_Charts_Metrics_Improvements
IVS-658 - Minor Django Admin charts & metrics improvements
2 parents d1c826d + bd73392 commit 19a4306

File tree

4 files changed

+112
-26
lines changed

4 files changed

+112
-26
lines changed

backend/apps/ifc_validation/chart_urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
path("avg-size/<int:year>/", charts.get_avg_size_chart),
1515
path("user-registrations/<int:year>/", charts.get_user_registrations_chart),
1616
path("usage-by-vendor/<int:year>/", charts.get_usage_by_vendor_chart),
17+
path("usage-by-channel/<int:year>/", charts.get_usage_by_channel_chart),
1718
path("models-by-vendor/<int:year>/", charts.get_models_by_vendor_chart),
1819
path("top-tools/<int:year>/", charts.get_top_tools_chart),
1920
path("tools-count/<int:year>/", charts.get_tools_count_chart),

backend/apps/ifc_validation/chart_views.py

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
AuthoringTool,
1818
)
1919

20-
months = list(calendar.month_name)[1:]
20+
MONTHS = list(calendar.month_name)[1:]
2121

2222
PERIODS = {
2323
"month": {
2424
"annotate": lambda qs: qs.annotate(period=ExtractMonth("created")),
25-
"label": lambda row: months[row["period"] - 1],
26-
"full_set": months,
25+
"label": lambda row: MONTHS[row["period"] - 1],
26+
"full_set": MONTHS,
2727
},
2828
"week": {
2929
"annotate": lambda qs: qs.annotate(period=ExtractWeek("created")),
@@ -52,7 +52,6 @@
5252
},
5353
}
5454

55-
5655
COLORS = {
5756
"primary": "#79aec8",
5857
"success": "#55efc4",
@@ -73,7 +72,6 @@
7372
"HEADER_SYNTAX": "SYNTAX",
7473
}
7574

76-
7775
TASK_TYPES = {
7876
"SYNTAX": ("Syntax", COLORS["success"]),
7977
"SCHEMA": ("Schema", COLORS["schema"]),
@@ -120,7 +118,8 @@ def chart_response(title, labels, datasets):
120118
"datasets": datasets,
121119
},
122120
})
123-
121+
122+
124123
def group_by_period(qs, period, agg_key, agg_expression, window=None):
125124
cfg = PERIODS[period]
126125
qs = cfg["annotate"](qs)
@@ -131,7 +130,7 @@ def group_by_period(qs, period, agg_key, agg_expression, window=None):
131130
qs = qs.filter(
132131
Q(period__in=[
133132
int(lbl[1:]) if period in ["week", "quarter"]
134-
else months.index(lbl) + 1 if period == "month"
133+
else MONTHS.index(lbl) + 1 if period == "month"
135134
else lbl
136135
for lbl in valid_labels
137136
])
@@ -142,6 +141,7 @@ def group_by_period(qs, period, agg_key, agg_expression, window=None):
142141
.order_by("period")
143142
)
144143

144+
145145
def fill_period_dict(grouped_qs, period, year, *, key, transform=lambda x: x, window=None):
146146
data = dict_for_period(period, year=year, window=window)
147147
cfg = PERIODS[period]
@@ -153,9 +153,11 @@ def fill_period_dict(grouped_qs, period, year, *, key, transform=lambda x: x, wi
153153
data[label] = round(value, 2)
154154
return data
155155

156+
156157
def get_period(request):
157158
return request.GET.get("period", "month").lower()
158159

160+
159161
def get_window(request) -> int | None:
160162
try:
161163
w = int(request.GET.get("window", ""))
@@ -201,7 +203,7 @@ def _rolling_labels(period: str, window: int, today: datetime.date | None = None
201203
month_cursor = today.replace(day=1)
202204
labels = []
203205
for _ in range(window):
204-
labels.insert(0, months[month_cursor.month - 1])
206+
labels.insert(0, MONTHS[month_cursor.month - 1])
205207
month_cursor = (month_cursor - datetime.timedelta(days=1)).replace(day=1)
206208
return labels
207209

@@ -390,7 +392,7 @@ def get_duration_per_task_chart(request, year):
390392
valid_labels = set(_rolling_labels(period, window))
391393
label_values = [
392394
int(lbl[1:]) if period in ["week", "quarter"]
393-
else months.index(lbl) + 1 if period == "month"
395+
else MONTHS.index(lbl) + 1 if period == "month"
394396
else lbl
395397
for lbl in valid_labels
396398
]
@@ -437,7 +439,6 @@ def get_duration_per_task_chart(request, year):
437439
)
438440

439441

440-
441442
@staff_member_required
442443
def get_processing_status_chart(request, year):
443444
period = get_period(request)
@@ -629,6 +630,78 @@ def get_usage_by_vendor_chart(request, year):
629630
)
630631

631632

633+
@staff_member_required
634+
def get_usage_by_channel_chart(request, year):
635+
"""
636+
Distinct channels per period, split into WebUI vs API.
637+
"""
638+
period = get_period(request)
639+
window = get_window(request)
640+
641+
if period == "total":
642+
qs = ValidationRequest.objects.all()
643+
total_uploaders = qs.values("channel").distinct().count()
644+
api_uploaders = qs.filter(
645+
channel=ValidationRequest.Channel.API
646+
).values("id").distinct().count()
647+
webui_uploaders = total_uploaders - api_uploaders
648+
649+
return chart_response(
650+
title="Channels (WebUI vs API, Total)",
651+
labels=["WebUI", "API"],
652+
datasets=[{
653+
"label": "Channels",
654+
"backgroundColor": [COLORS["primary"], COLORS["success"]],
655+
"borderColor": [COLORS["primary"], COLORS["success"]],
656+
"data": [webui_uploaders, api_uploaders],
657+
}]
658+
)
659+
660+
qs = ValidationRequest.objects.filter(created__year=year)
661+
662+
total_qs = group_by_period(
663+
qs,
664+
period,
665+
"total",
666+
Count("id", distinct=True),
667+
window=window
668+
)
669+
670+
api_qs = group_by_period(
671+
qs.filter(channel=ValidationRequest.Channel.API),
672+
period,
673+
ValidationRequest.Channel.API,
674+
Count("id", distinct=True),
675+
window=window
676+
)
677+
678+
total_dict = fill_period_dict(total_qs, period, year, key="total", transform=int, window=window)
679+
api_dict = fill_period_dict(api_qs, period, year, key="API", transform=int, window=window)
680+
webui_dict = {lbl: total_dict[lbl] - api_dict.get(lbl, 0) for lbl in total_dict}
681+
labels = list(total_dict.keys())
682+
683+
return chart_response(
684+
title=f"Channels (WebUI vs API) in {year}",
685+
labels=labels,
686+
datasets=[
687+
{
688+
"label": "WebUI",
689+
"backgroundColor": COLORS["primary"],
690+
"borderColor": COLORS["primary"],
691+
"data": [webui_dict[lbl] for lbl in labels],
692+
"stack": "stack1",
693+
},
694+
{
695+
"label": "API",
696+
"backgroundColor": COLORS["success"],
697+
"borderColor": COLORS["success"],
698+
"data": [api_dict[lbl] for lbl in labels],
699+
"stack": "stack1",
700+
},
701+
],
702+
)
703+
704+
632705
@staff_member_required
633706
def get_models_by_vendor_chart(request, year):
634707
"""
@@ -676,7 +749,7 @@ def get_models_by_vendor_chart(request, year):
676749
valid_labels = set(_rolling_labels(period, window))
677750
label_values = [
678751
int(lbl[1:]) if period in ["week", "quarter"]
679-
else months.index(lbl) + 1 if period == "month"
752+
else MONTHS.index(lbl) + 1 if period == "month"
680753
else lbl
681754
for lbl in valid_labels
682755
]
@@ -812,7 +885,6 @@ def get_tools_count_chart(request, year):
812885
)
813886

814887

815-
816888
@staff_member_required
817889
def get_totals(request):
818890
# Overall, non time-split totals
@@ -851,7 +923,7 @@ def get_uploads_per_2h_chart(request, year):
851923
valid_labels = set(_rolling_labels(period, window))
852924
label_values = [
853925
int(lbl[1:]) if period in ["week", "quarter"]
854-
else months.index(lbl) + 1 if period == "month"
926+
else MONTHS.index(lbl) + 1 if period == "month"
855927
else lbl
856928
for lbl in valid_labels
857929
]
@@ -883,6 +955,7 @@ def get_uploads_per_2h_chart(request, year):
883955
}],
884956
)
885957

958+
886959
@staff_member_required
887960
def get_queue_p95_chart(request, year):
888961
period = get_period(request)
@@ -899,7 +972,7 @@ def get_queue_p95_chart(request, year):
899972
valid_labels = set(_rolling_labels(period, window))
900973
label_values = [
901974
int(lbl[1:]) if period in ["week", "quarter"]
902-
else months.index(lbl) + 1 if period == "month"
975+
else MONTHS.index(lbl) + 1 if period == "month"
903976
else lbl
904977
for lbl in valid_labels
905978
]
@@ -965,7 +1038,7 @@ def get_stuck_per_day_chart(request, year):
9651038
valid_labels = set(_rolling_labels(period, window))
9661039
label_values = [
9671040
int(lbl[1:]) if period in ["week", "quarter"]
968-
else months.index(lbl) + 1 if period == "month"
1041+
else MONTHS.index(lbl) + 1 if period == "month"
9691042
else lbl
9701043
for lbl in valid_labels
9711044
]
@@ -997,6 +1070,7 @@ def get_stuck_per_day_chart(request, year):
9971070
}],
9981071
)
9991072

1073+
10001074
@staff_member_required
10011075
def get_uploads_per_weekday_chart(request, year):
10021076
period = get_period(request)
@@ -1013,7 +1087,7 @@ def get_uploads_per_weekday_chart(request, year):
10131087
valid_labels = set(_rolling_labels(period, window))
10141088
label_values = [
10151089
int(lbl[1:]) if period in ["week", "quarter"]
1016-
else months.index(lbl) + 1 if period == "month"
1090+
else MONTHS.index(lbl) + 1 if period == "month"
10171091
else lbl
10181092
for lbl in valid_labels
10191093
]

backend/apps/ifc_validation/templates/admin/ifc_validation_models/app_index.html

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@
1414
{% endblock %}
1515
{% endif %}
1616
{% block content %}
17+
1718
<div style="float: left">
1819
<div id="content-main">
1920
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
2021
</div>
2122

2223
<div id="content-statistics">
23-
<h1>Statistics</h1>
24+
<h1>Metrics &amp; Statistics</h1>
2425
<style>
26+
.dashboard #content { width: 80%; }
27+
.charts-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; min-width: 1400px; margin-top: 35px; }
28+
.chart-cell { width: 100%; min-height: 350px; } */
2529
.kpi-tile {
2630
padding: 1rem;
2731
background: #333;
@@ -43,6 +47,7 @@ <h1>Statistics</h1>
4347
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
4448
crossorigin="anonymous">
4549
</script>
50+
4651
<form id="filterForm">
4752
<span id="year-wrapper">
4853
<label for="year">Choose a year:</label>
@@ -86,17 +91,18 @@ <h1>Statistics</h1>
8691
{ id: "requestsChart", type: "bar", endpoint: "requests", stacked: true },
8792
{ id: "durationPerRequestChart",type: "line", endpoint: "duration-per-request", tooltip: ctx => `${ctx.parsed.y} m` },
8893
{ id: "durationPerTaskChart", type: "bar", endpoint: "duration-per-task", stacked: true },
89-
{ id: "processingStatusChart", type: "pie", endpoint: "processing-status" },
94+
// { id: "processingStatusChart", type: "pie", endpoint: "processing-status" },
9095
{ id: "avgSizeChart", type: "line", endpoint: "avg-size" },
91-
{ id: "uploadsBy2hChart", type: "bar", endpoint: "uploads-per-2h", tooltip: ctx => `${ctx.parsed.y} uploads` },
96+
{ id: "uploadsBy2hChart", type: "bar", endpoint: "uploads-per-2h", tooltip: ctx => `${ctx.parsed.y} uploads` },
9297
{ id: "userRegistrationsChart", type: "bar", endpoint: "user-registrations", tooltip: ctx => `${ctx.parsed.y} user${ctx.parsed.y === 1 ? "" : "s"}` },
98+
{ id: "usageByChannelChart", type: "bar", endpoint: "usage-by-channel", stacked: true, tooltip: ctx => `${ctx.parsed.y} ${ctx.dataset.label.toLowerCase()}` },
9399
{ id: "usageByVendorChart", type: "bar", endpoint: "usage-by-vendor", stacked: true, tooltip: ctx => `${ctx.parsed.y} ${ctx.dataset.label.toLowerCase()}` },
94100
{ id: "modelsByVendorChart", type: "bar", endpoint: "models-by-vendor", stacked: true, tooltip: ctx => `${ctx.parsed.y} ${ctx.dataset.label.toLowerCase()}` },
95101
{ id: "topToolsChart", type: "bar", endpoint: "top-tools", horizontal: true, tooltip: ctx => `${ctx.parsed.x} model${ctx.parsed.x === 1 ? "" : "s"}` },
96102
{ id: "toolsCountChart", type: "bar", endpoint: "tools-count", tooltip: ctx => `${ctx.parsed.y} tool${ctx.parsed.y === 1 ? "" : "s"}` },
97-
{ id: "queueP95Chart", type: "line", endpoint: "queue-p95", tooltip: ctx => `${ctx.parsed.y} s`},
98-
{ id: "stuckPerDayChart", type: "bar", endpoint: "stuck-per-day", tooltip: ctx => `${ctx.parsed.y} stuck`},
99-
{ id: "uploadsPerWeekdayChart", type: "bar", endpoint: "uploads-per-weekday", tooltip: ctx => `${ctx.parsed.y} uploads`},
103+
{ id: "queueP95Chart", type: "line", endpoint: "queue-p95", tooltip: ctx => `${ctx.parsed.y} s`},
104+
{ id: "stuckPerDayChart", type: "bar", endpoint: "stuck-per-day", tooltip: ctx => `${ctx.parsed.y} stuck`},
105+
{ id: "uploadsPerWeekdayChart", type: "bar", endpoint: "uploads-per-weekday", tooltip: ctx => `${ctx.parsed.y} uploads`},
100106
];
101107

102108
const charts = {}; // Chart instances keyed by id
@@ -106,9 +112,13 @@ <h1>Statistics</h1>
106112
// ------------------------------------------------------------------
107113
function createCanvases() {
108114
const $container = $("#charts");
109-
$container.empty();
115+
const $grid = $('<div>', { class: 'charts-grid' });
116+
$container.empty();
117+
$container.append($grid);
110118
chartDefs.forEach(def => {
111-
$("<canvas>", { id: def.id, class: "my-4" }).appendTo($container);
119+
const $cell = $('<div>', { class: 'chart-cell' });
120+
$("<canvas>", { id: def.id, class: "my-4", style: "height: 100%; width: 100%" }).appendTo($cell);
121+
$cell.appendTo($grid);
112122
});
113123
}
114124

@@ -198,7 +208,6 @@ <h3>${title}</h3>
198208
return;
199209
}
200210

201-
202211
if (def.id === "usageByVendorChart" && isTotal) {
203212
const $canvas = $("#" + def.id);
204213
const [endUsers, vendors] = data.datasets[0].data;
@@ -285,5 +294,7 @@ <h3>${title}</h3>
285294
})(jQuery);
286295
</script>
287296
</div>
297+
288298
</div>
299+
289300
{% endblock %}

e2e/tests/django_admin.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ test.describe('UI - Django Admin', () => {
135135

136136
// check some charts
137137
await expect(page.locator('#requestsChart')).toBeVisible();
138-
await expect(page.locator('#processingStatusChart')).toBeVisible();
139138
await expect(page.locator('#usageByVendorChart')).toBeVisible();
139+
await expect(page.locator('#usageByChannelChart')).toBeVisible();
140140
await expect(page.locator('#topToolsChart')).toBeVisible();
141141
await expect(page.locator('#queueP95Chart')).toBeVisible();
142142

0 commit comments

Comments
 (0)