Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions dandiapi/api/tests/test_content_modified.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Tests for the content_modified field reflecting version content changes."""

from __future__ import annotations

import datetime

from django.utils import timezone
import pytest

from dandiapi.api.models import Dandiset, Version
from dandiapi.api.tests.factories import (
DraftVersionFactory,
)


@pytest.mark.django_db
def test_list_content_modified_reflects_version_modified(api_client):
"""The listing endpoint's content_modified field should reflect the version's modified."""
draft_version = DraftVersionFactory.create()
dandiset = draft_version.dandiset

# Set the dandiset's own modified to something old
old_time = timezone.now() - datetime.timedelta(days=365)
Dandiset.objects.filter(id=dandiset.id).update(modified=old_time)

response = api_client.get(
'/api/dandisets/', {'draft': 'true', 'empty': 'true'}
)
assert response.status_code == 200
result = response.data['results'][0]

draft_version.refresh_from_db()
assert result['content_modified'] == draft_version.modified.isoformat().replace(
'+00:00', 'Z'
)


@pytest.mark.django_db
def test_retrieve_content_modified_reflects_version_modified(api_client):
"""The retrieve endpoint's content_modified field should reflect the version's modified."""
draft_version = DraftVersionFactory.create()
dandiset = draft_version.dandiset

old_time = timezone.now() - datetime.timedelta(days=365)
Dandiset.objects.filter(id=dandiset.id).update(modified=old_time)

response = api_client.get(f'/api/dandisets/{dandiset.identifier}/')
assert response.status_code == 200

draft_version.refresh_from_db()
assert response.data['content_modified'] == draft_version.modified.isoformat().replace(
'+00:00', 'Z'
)


@pytest.mark.django_db
def test_ordering_by_content_modified(api_client):
"""Ordering by content_modified should sort by version modified timestamps."""
now = timezone.now()

# Create 3 dandisets with draft versions
v1 = DraftVersionFactory.create()
v2 = DraftVersionFactory.create()
v3 = DraftVersionFactory.create()

# Set different version modified times
Version.objects.filter(id=v1.id).update(
modified=now - datetime.timedelta(days=3)
)
Version.objects.filter(id=v2.id).update(
modified=now - datetime.timedelta(days=1)
)
Version.objects.filter(id=v3.id).update(
modified=now - datetime.timedelta(days=2)
)

# Ascending
response = api_client.get(
'/api/dandisets/',
{'ordering': 'content_modified', 'draft': 'true', 'empty': 'true'},
)
assert response.status_code == 200
ids = [r['identifier'] for r in response.data['results']]
assert ids == [
v1.dandiset.identifier,
v3.dandiset.identifier,
v2.dandiset.identifier,
]

# Descending
response = api_client.get(
'/api/dandisets/',
{'ordering': '-content_modified', 'draft': 'true', 'empty': 'true'},
)
assert response.status_code == 200
ids = [r['identifier'] for r in response.data['results']]
assert ids == [
v2.dandiset.identifier,
v3.dandiset.identifier,
v1.dandiset.identifier,
]
15 changes: 8 additions & 7 deletions dandiapi/api/views/dandiset.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@


class DandisetOrderingFilter(filters.OrderingFilter):
ordering_fields = ['id', 'name', 'modified', 'size', 'stars']
ordering_fields = ['id', 'name', 'modified', 'content_modified', 'size', 'stars']
ordering_description = (
'Which field to use when ordering the results. '
'Options are id, -id, name, -name, modified, -modified, size, -size, stars, -stars.'
'Options are id, -id, name, -name, modified, -modified, '
'content_modified, -content_modified, size, -size, stars, -stars.'
)

def filter_queryset(self, request, queryset, view):
Expand All @@ -96,17 +97,17 @@ def filter_queryset(self, request, queryset, view):
queryset = queryset.annotate(
name=Subquery(latest_version.values('metadata__name'))
).order_by(ordering)
elif ordering.endswith('modified'):
# modified refers to the modification timestamp of the most
elif ordering.endswith('content_modified') or ordering.endswith('modified'):
# content_modified/modified refers to the modification timestamp of the most
# recent version, so a subquery is required
latest_version = Version.objects.filter(dandiset=OuterRef('pk')).order_by('-created')[
:1
]
# get the `modified` field of the most recent version.
# '_version' is appended because the Dandiset model already has a `modified` field
prefix = '-' if ordering.startswith('-') else ''
queryset = queryset.annotate(
modified_version=Subquery(latest_version.values('modified'))
).order_by(f'{ordering}_version')
_content_modified=Subquery(latest_version.values('modified'))
).order_by(f'{prefix}_content_modified')
elif ordering.endswith('size'):
latest_version = Version.objects.filter(dandiset=OuterRef('pk')).order_by('-created')[
:1
Expand Down
29 changes: 26 additions & 3 deletions dandiapi/api/views/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class DandisetListSerializer(DandisetSerializer):
"""The dandiset serializer to be used in the listing endpoint."""

class Meta(DandisetSerializer.Meta):
fields = [*DandisetSerializer.Meta.fields, 'most_recent_published_version', 'draft_version']
fields = [*DandisetSerializer.Meta.fields, 'content_modified', 'most_recent_published_version', 'draft_version']

@swagger_serializer_method(serializer_or_field=DandisetVersionSerializer)
def get_draft_version(self, dandiset):
Expand Down Expand Up @@ -230,12 +230,23 @@ def get_contact_person(self, dandiset):

return contact

def get_content_modified(self, dandiset):
"""Return the most recent version's modified timestamp (content change time)."""
draft = self.context['dandisets'].get(dandiset.id, {}).get('draft')
if draft is not None:
return draft.modified
published = self.context['dandisets'].get(dandiset.id, {}).get('published')
if published is not None:
return published.modified
return dandiset.modified

def get_star_count(self, dandiset):
return self.context['stars'][dandiset.id]['total']

def get_is_starred(self, dandiset):
return self.context['stars'][dandiset.id]['starred_by_current_user']

content_modified = serializers.SerializerMethodField()
most_recent_published_version = serializers.SerializerMethodField()
draft_version = serializers.SerializerMethodField()

Expand All @@ -252,8 +263,20 @@ def get_asset_counts(self, dandiset):

class DandisetDetailSerializer(DandisetSerializer):
class Meta(DandisetSerializer.Meta):
fields = [*DandisetSerializer.Meta.fields, 'most_recent_published_version', 'draft_version']

fields = [*DandisetSerializer.Meta.fields, 'content_modified', 'most_recent_published_version', 'draft_version']

def get_content_modified(self, dandiset):
"""Return the most recent version's modified timestamp (content change time)."""
try:
return dandiset.draft_version.modified
except Version.DoesNotExist:
pass
mrpv = dandiset.most_recent_published_version
if mrpv is not None:
return mrpv.modified
return dandiset.modified

content_modified = serializers.SerializerMethodField()
most_recent_published_version = VersionSerializer(read_only=True, child_context=True)
draft_version = VersionSerializer(read_only=True, child_context=True)

Expand Down
Loading