Downloads / GB by user region
+
+ Requests in range — Files: {{ download_events_dashboard.split.file.count }}
+ · Zips: {{ download_events_dashboard.split.zip.count }}
+ · Total: {{ download_events_dashboard.summary.total_downloads }}
+
@@ -296,6 +311,24 @@
{% endfor %}
];
+const storageFileCounts = [
+ {% for region in download_events_dashboard.storage_regions %}
+ {{ region.file_count }},
+ {% endfor %}
+];
+
+const storageZipCounts = [
+ {% for region in download_events_dashboard.storage_regions %}
+ {{ region.zip_count }},
+ {% endfor %}
+];
+
+const storageTotalCounts = [
+ {% for region in download_events_dashboard.storage_regions %}
+ {{ region.downloads }},
+ {% endfor %}
+];
+
const userRegionLabels = [
{% for region in download_events_dashboard.user_regions %}
"{{ region.name|escapejs }}",
@@ -314,6 +347,24 @@
{% endfor %}
];
+const userFileCounts = [
+ {% for region in download_events_dashboard.user_regions %}
+ {{ region.file_count }},
+ {% endfor %}
+];
+
+const userZipCounts = [
+ {% for region in download_events_dashboard.user_regions %}
+ {{ region.zip_count }},
+ {% endfor %}
+];
+
+const userTotalCounts = [
+ {% for region in download_events_dashboard.user_regions %}
+ {{ region.downloads }},
+ {% endfor %}
+];
+
new Chart(
document.getElementById("gbOverTimeChart"),
{
@@ -406,6 +457,17 @@
...commonOptions.plugins,
legend: {
display: false
+ },
+ tooltip: {
+ ...commonOptions.plugins.tooltip,
+ callbacks: {
+ footer: function (items) {
+ const i = items[0].dataIndex;
+ return "Files: " + storageFileCounts[i] +
+ " · Zips: " + storageZipCounts[i] +
+ " · Total: " + storageTotalCounts[i];
+ }
+ }
}
},
scales: {
@@ -456,6 +518,20 @@
options: {
...commonOptions,
indexAxis: "y",
+ plugins: {
+ ...commonOptions.plugins,
+ tooltip: {
+ ...commonOptions.plugins.tooltip,
+ callbacks: {
+ footer: function (items) {
+ const i = items[0].dataIndex;
+ return "Files: " + userFileCounts[i] +
+ " · Zips: " + userZipCounts[i] +
+ " · Total: " + userTotalCounts[i];
+ }
+ }
+ }
+ },
scales: {
x: {
diff --git a/osf/admin.py b/osf/admin.py
index c527981330f..284b8695f66 100644
--- a/osf/admin.py
+++ b/osf/admin.py
@@ -736,18 +736,27 @@ def _format_bucket_label(self, value, bucket_size):
return value.strftime('%Y-%m-%d')
def _build_region_breakdown(self, queryset, field_name):
- """Grouped in the database — the range can cover millions of rows."""
+ """Grouped in the database — the range can cover millions of rows.
+
+ `downloads` is the total request count; `file_count` and `zip_count` split it by
+ request type (a zip is either a folder or a whole-project zip), so file + zip always
+ equals the total.
+ """
rows = queryset.values(field_name).annotate(
downloads=Count('id'),
total_bytes=Sum('size_bytes'),
+ file_count=Count('id', filter=Q(download_type=DownloadEvent.FILE)),
+ zip_count=Count('id', filter=~Q(download_type=DownloadEvent.FILE)),
)
- breakdown = defaultdict(lambda: {'downloads': 0, 'gb': 0.0})
+ breakdown = defaultdict(lambda: {'downloads': 0, 'gb': 0.0, 'file_count': 0, 'zip_count': 0})
for row in rows:
# blank and null both mean "we could not tell", so they fold together
region_name = (row[field_name] or 'Unknown').strip() or 'Unknown'
breakdown[region_name]['downloads'] += row['downloads']
breakdown[region_name]['gb'] += (row['total_bytes'] or 0) / (1024**3)
+ breakdown[region_name]['file_count'] += row['file_count']
+ breakdown[region_name]['zip_count'] += row['zip_count']
ordered = sorted(breakdown.items(), key=lambda item: item[1]['gb'], reverse=True)[:10]
max_gb = max((data['gb'] for _, data in ordered), default=0)
@@ -756,6 +765,8 @@ def _build_region_breakdown(self, queryset, field_name):
{
'name': name,
'downloads': data['downloads'],
+ 'file_count': data['file_count'],
+ 'zip_count': data['zip_count'],
'gb': round(data['gb'], 2),
'gb_percent': self._percent(data['gb'], max_gb),
'download_percent': self._percent(data['downloads'], max_downloads),
diff --git a/tests/test_download_events_dashboard.py b/tests/test_download_events_dashboard.py
index 2cdf607bf8e..5cdfefabcad 100644
--- a/tests/test_download_events_dashboard.py
+++ b/tests/test_download_events_dashboard.py
@@ -197,6 +197,40 @@ def test_blank_storage_provider_folds_into_unknown(self):
assert [row['name'] for row in providers] == ['Unknown']
+ def test_region_breakdown_splits_requests_by_type(self):
+ # Germany: 2 files + 1 folder zip + 1 project zip = 4 total, 3 zips
+ make_event(storage_region='Germany', download_type=DownloadEvent.FILE)
+ make_event(storage_region='Germany', download_type=DownloadEvent.FILE)
+ make_event(storage_region='Germany', download_type=DownloadEvent.FOLDER_ZIP)
+ make_event(storage_region='Germany', download_type=DownloadEvent.PROJECT)
+
+ regions = self.admin.get_dashboard_data(DownloadEvent.objects.all())['storage_regions']
+ germany = next(r for r in regions if r['name'] == 'Germany')
+
+ assert germany['file_count'] == 2
+ assert germany['zip_count'] == 2
+ # file + zip always equals the total — the invariant QA will check
+ assert germany['file_count'] + germany['zip_count'] == germany['downloads'] == 4
+
+ def test_region_type_counts_match_the_period_totals(self):
+ """Summed across regions, file/zip counts equal the header's split totals — so the
+ subtitle and the bars can never disagree."""
+ make_event(storage_region='Germany', download_type=DownloadEvent.FILE)
+ make_event(storage_region='United States', download_type=DownloadEvent.FILE)
+ make_event(storage_region='Germany', download_type=DownloadEvent.FOLDER_ZIP)
+
+ data = self.admin.get_dashboard_data(DownloadEvent.objects.all())
+ regions = data['storage_regions']
+
+ assert sum(r['file_count'] for r in regions) == data['split']['file']['count'] == 2
+ assert sum(r['zip_count'] for r in regions) == data['split']['zip']['count'] == 1
+
+ def test_region_type_counts_present_even_when_empty(self):
+ data = self.admin.get_dashboard_data(DownloadEvent.objects.none())
+
+ assert data['storage_regions'] == []
+ assert data['user_regions'] == []
+
def test_blank_and_null_regions_fold_into_unknown(self):
make_event(storage_region='')
make_event(storage_region=' ')