From 413e4adf6caa5b47c803f064726147098a8013e5 Mon Sep 17 00:00:00 2001 From: Jacob Nesbitt Date: Fri, 3 Jan 2025 16:08:46 -0500 Subject: [PATCH] Add audit events for dandiset stars --- dandiapi/api/migrations/0014_dandisetstar.py | 31 ++++++++++++++++---- dandiapi/api/models/audit.py | 2 ++ dandiapi/api/services/audit/__init__.py | 22 ++++++++++++++ dandiapi/api/services/dandiset/__init__.py | 16 +++++++--- dandiapi/api/tests/test_dandiset.py | 19 ++++++++++++ 5 files changed, 81 insertions(+), 9 deletions(-) diff --git a/dandiapi/api/migrations/0014_dandisetstar.py b/dandiapi/api/migrations/0014_dandisetstar.py index b9bf49a7c..30f26ffc1 100644 --- a/dandiapi/api/migrations/0014_dandisetstar.py +++ b/dandiapi/api/migrations/0014_dandisetstar.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.17 on 2024-12-26 14:24 +# Generated by Django 4.2.17 on 2025-01-03 21:07 from __future__ import annotations from django.conf import settings @@ -13,16 +13,37 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AlterField( + model_name='auditrecord', + name='record_type', + field=models.CharField( + choices=[ + ('create_dandiset', 'create_dandiset'), + ('change_owners', 'change_owners'), + ('update_metadata', 'update_metadata'), + ('add_asset', 'add_asset'), + ('update_asset', 'update_asset'), + ('remove_asset', 'remove_asset'), + ('create_zarr', 'create_zarr'), + ('upload_zarr_chunks', 'upload_zarr_chunks'), + ('delete_zarr_chunks', 'delete_zarr_chunks'), + ('finalize_zarr', 'finalize_zarr'), + ('unembargo_dandiset', 'unembargo_dandiset'), + ('publish_dandiset', 'publish_dandiset'), + ('star_dandiset', 'star_dandiset'), + ('unstar_dandiset', 'unstar_dandiset'), + ('delete_dandiset', 'delete_dandiset'), + ], + max_length=32, + ), + ), migrations.CreateModel( name='DandisetStar', fields=[ ( 'id', models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' ), ), ('created', models.DateTimeField(auto_now_add=True)), diff --git a/dandiapi/api/models/audit.py b/dandiapi/api/models/audit.py index 56d5254f7..dddd89bdf 100644 --- a/dandiapi/api/models/audit.py +++ b/dandiapi/api/models/audit.py @@ -17,6 +17,8 @@ 'finalize_zarr', 'unembargo_dandiset', 'publish_dandiset', + 'star_dandiset', + 'unstar_dandiset', 'delete_dandiset', ] AUDIT_RECORD_CHOICES = [(t, t) for t in get_args(AuditRecordType)] diff --git a/dandiapi/api/services/audit/__init__.py b/dandiapi/api/services/audit/__init__.py index adad72b1a..87cc8ccb1 100644 --- a/dandiapi/api/services/audit/__init__.py +++ b/dandiapi/api/services/audit/__init__.py @@ -160,6 +160,28 @@ def publish_dandiset(*, dandiset: Dandiset, user: User, version: str) -> AuditRe ) +def star_dandiset(*, dandiset: Dandiset, user: User, new_star_count: int) -> AuditRecord: + return _make_audit_record( + dandiset=dandiset, + user=user, + record_type='star_dandiset', + details={ + 'new_star_count': new_star_count, + }, + ) + + +def unstar_dandiset(*, dandiset: Dandiset, user: User, new_star_count: int) -> AuditRecord: + return _make_audit_record( + dandiset=dandiset, + user=user, + record_type='unstar_dandiset', + details={ + 'new_star_count': new_star_count, + }, + ) + + def delete_dandiset(*, dandiset: Dandiset, user: User): return _make_audit_record( dandiset=dandiset, user=user, record_type='delete_dandiset', details={} diff --git a/dandiapi/api/services/dandiset/__init__.py b/dandiapi/api/services/dandiset/__init__.py index 7547b089a..352c947db 100644 --- a/dandiapi/api/services/dandiset/__init__.py +++ b/dandiapi/api/services/dandiset/__init__.py @@ -90,8 +90,12 @@ def star_dandiset(*, user, dandiset: Dandiset) -> int: if not user.is_authenticated: return dandiset.star_count - DandisetStar.objects.get_or_create(user=user, dandiset=dandiset) - return dandiset.star_count + with transaction.atomic(): + DandisetStar.objects.get_or_create(user=user, dandiset=dandiset) + new_star_count = dandiset.star_count + audit.star_dandiset(dandiset=dandiset, user=user, new_star_count=new_star_count) + + return new_star_count def unstar_dandiset(*, user, dandiset: Dandiset) -> int: @@ -108,5 +112,9 @@ def unstar_dandiset(*, user, dandiset: Dandiset) -> int: if not user.is_authenticated: return dandiset.star_count - DandisetStar.objects.filter(user=user, dandiset=dandiset).delete() - return dandiset.star_count + with transaction.atomic(): + DandisetStar.objects.filter(user=user, dandiset=dandiset).delete() + new_star_count = dandiset.star_count + audit.unstar_dandiset(dandiset=dandiset, user=user, new_star_count=new_star_count) + + return new_star_count diff --git a/dandiapi/api/tests/test_dandiset.py b/dandiapi/api/tests/test_dandiset.py index ada6b52a5..6f7a022ce 100644 --- a/dandiapi/api/tests/test_dandiset.py +++ b/dandiapi/api/tests/test_dandiset.py @@ -9,6 +9,7 @@ from dandiapi.api.asset_paths import add_asset_paths, add_version_asset_paths from dandiapi.api.models import Dandiset, Version +from dandiapi.api.models.audit import AuditRecord from .fuzzy import DANDISET_ID_RE, DANDISET_SCHEMA_ID_RE, TIMESTAMP_RE, UTC_ISO_TIMESTAMP_RE @@ -1230,12 +1231,23 @@ def test_dandiset_rest_clear_active_uploads( @pytest.mark.django_db def test_dandiset_star(api_client, user, dandiset): api_client.force_authenticate(user=user) + assert not AuditRecord.objects.filter(record_type='star_dandiset').exists() + assert not dandiset.stars.exists() + response = api_client.post(f'/api/dandisets/{dandiset.identifier}/star/') assert response.status_code == 200 assert response.data == {'count': 1} + + # Check for star db entry assert dandiset.stars.count() == 1 assert dandiset.stars.first().user == user + # Check for audit record + assert AuditRecord.objects.filter(record_type='star_dandiset').count() == 1 + audit_record = AuditRecord.objects.get(record_type='star_dandiset') + assert audit_record.dandiset_id == dandiset.id + assert audit_record.username == user.username + @pytest.mark.django_db def test_dandiset_unstar(api_client, user, dandiset): @@ -1250,6 +1262,13 @@ def test_dandiset_unstar(api_client, user, dandiset): assert response.data == {'count': 0} assert dandiset.stars.count() == 0 + # Check for audit records + assert AuditRecord.objects.filter(dandiset_id=dandiset.id).count() == 2 + assert AuditRecord.objects.filter(record_type='unstar_dandiset').count() == 1 + audit_record = AuditRecord.objects.get(record_type='unstar_dandiset') + assert audit_record.dandiset_id == dandiset.id + assert audit_record.username == user.username + @pytest.mark.django_db def test_dandiset_star_unauthenticated(api_client, dandiset):