Skip to content

Commit 3068762

Browse files
committed
feat(osf): ENG-11734 add DownloadEvent model and migration with dashboard access group
1 parent f9cdf85 commit 3068762

3 files changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import django.db.models.deletion
2+
from django.db import migrations, models
3+
4+
5+
DASHBOARD_GROUP_NAME = 'download_telemetry'
6+
7+
DASHBOARD_USERS = [
8+
'sheredko.andriy@gmail.com',
9+
'bodintsov@exoft.net',
10+
'isokhan@exoft.net',
11+
'ykopka@exoft.net',
12+
'bgeiger@cos.io',
13+
'osmand@cos.io',
14+
]
15+
16+
17+
def create_dashboard_group(apps, schema_editor):
18+
"""Create the allow-list group the dashboard loads against and seed it.
19+
20+
The group carries no permissions of its own — membership is the only gate.
21+
"""
22+
Group = apps.get_model('auth', 'Group')
23+
OSFUser = apps.get_model('osf', 'OSFUser')
24+
group, _ = Group.objects.get_or_create(name=DASHBOARD_GROUP_NAME)
25+
for identifier in DASHBOARD_USERS:
26+
user = (
27+
OSFUser.objects.filter(username=identifier).first()
28+
or OSFUser.objects.filter(guids___id=identifier).first()
29+
)
30+
if user:
31+
group.user_set.add(user)
32+
33+
34+
def remove_dashboard_group(apps, schema_editor):
35+
Group = apps.get_model('auth', 'Group')
36+
Group.objects.filter(name=DASHBOARD_GROUP_NAME).delete()
37+
38+
39+
class Migration(migrations.Migration):
40+
41+
dependencies = [
42+
('osf', '0044_notification_scheduled'),
43+
]
44+
45+
operations = [
46+
migrations.CreateModel(
47+
name='DownloadEvent',
48+
fields=[
49+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
50+
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
51+
('resource_guid', models.CharField(blank=True, db_index=True, default='', max_length=255)),
52+
('path', models.TextField(blank=True, default='')),
53+
('name', models.CharField(blank=True, default='', max_length=255)),
54+
('download_type', models.CharField(choices=[('file', 'Single file'), ('folder_zip', 'Folder zip'), ('project', 'Whole-project zip')], db_index=True, max_length=16)),
55+
('zip_completed', models.BooleanField(blank=True, null=True)),
56+
('size_bytes', models.BigIntegerField(blank=True, null=True)),
57+
('storage_region', models.CharField(blank=True, db_index=True, default='', max_length=64)),
58+
('user_region', models.CharField(blank=True, db_index=True, default='', max_length=64)),
59+
('ip', models.GenericIPAddressField(blank=True, null=True)),
60+
('source_area', models.CharField(blank=True, default='', max_length=128)),
61+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='download_events', to='osf.osfuser')),
62+
],
63+
),
64+
migrations.AddIndex(
65+
model_name='downloadevent',
66+
index=models.Index(fields=['created', 'download_type'], name='download_event_crt_type'),
67+
),
68+
migrations.AddIndex(
69+
model_name='downloadevent',
70+
index=models.Index(fields=['created', 'storage_region'], name='download_event_crt_regn'),
71+
),
72+
migrations.RunPython(create_dashboard_group, remove_dashboard_group),
73+
]

osf/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .admin_log_entry import AdminLogEntry
1313
from .admin_profile import AdminProfile
1414
from .analytics import UserActivityCounter, PageCounter
15+
from .download_event import DownloadEvent
1516
from .archive import ArchiveJob, ArchiveTarget
1617
from .banner import ScheduledBanner
1718
from .base import (

osf/models/download_event.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from django.db import models
2+
3+
4+
class DownloadEvent(models.Model):
5+
"""One metadata row per download — never the file contents.
6+
7+
Foundation for the download-telemetry capture and dashboard. Append-only:
8+
rows are written from the download flow (single files at the osf.io redirect
9+
view, folder/project zips from the WaterButler callback) and read, always
10+
scoped to a time range, by the dashboard.
11+
"""
12+
13+
FILE = 'file'
14+
FOLDER_ZIP = 'folder_zip'
15+
PROJECT = 'project'
16+
DOWNLOAD_TYPES = (
17+
(FILE, 'Single file'),
18+
(FOLDER_ZIP, 'Folder zip'),
19+
(PROJECT, 'Whole-project zip'),
20+
)
21+
22+
created = models.DateTimeField(auto_now_add=True, db_index=True)
23+
24+
# what was downloaded
25+
resource_guid = models.CharField(max_length=255, blank=True, default='', db_index=True)
26+
path = models.TextField(blank=True, default='')
27+
name = models.CharField(max_length=255, blank=True, default='')
28+
download_type = models.CharField(max_length=16, choices=DOWNLOAD_TYPES, db_index=True)
29+
# null for single files (only zips stream through WB, which reports completion)
30+
zip_completed = models.BooleanField(null=True, blank=True)
31+
size_bytes = models.BigIntegerField(null=True, blank=True)
32+
33+
# storage_region = where the bytes were served from (capacity);
34+
# user_region = roughly where the user is. Kept separate on purpose.
35+
storage_region = models.CharField(max_length=64, blank=True, default='', db_index=True)
36+
user_region = models.CharField(max_length=64, blank=True, default='', db_index=True)
37+
ip = models.GenericIPAddressField(null=True, blank=True)
38+
source_area = models.CharField(max_length=128, blank=True, default='')
39+
40+
# nullable: anonymous downloads of public files
41+
user = models.ForeignKey(
42+
'osf.OSFUser',
43+
null=True,
44+
blank=True,
45+
on_delete=models.SET_NULL,
46+
related_name='download_events',
47+
)
48+
49+
class Meta:
50+
# `created` is indexed on the field; these cover the dashboard's
51+
# time-range group-bys.
52+
indexes = [
53+
models.Index(fields=['created', 'download_type'], name='download_event_crt_type'),
54+
models.Index(fields=['created', 'storage_region'], name='download_event_crt_regn'),
55+
]
56+
57+
def __repr__(self):
58+
return (
59+
f'<DownloadEvent(id={self.id}, user={self.user_id}, '
60+
f'type={self.download_type}, size_bytes={self.size_bytes})>'
61+
)
62+
63+
def __str__(self):
64+
return self.__repr__()

0 commit comments

Comments
 (0)