Skip to content

Commit

Permalink
Expose status on collection and entity viewset
Browse files Browse the repository at this point in the history
and allow filtering and sorting by it
  • Loading branch information
gregorjerse committed Nov 25, 2024
1 parent 0f14a96 commit 5021482
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 44 deletions.
10 changes: 10 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ All notable changes to this project are documented in this file.
This project adheres to `Semantic Versioning <http://semver.org/>`_.


==========
Unreleased
==========

Added
-----
- Expose ``status`` on ``collection`` and ``entity`` viewset and allow
filtering and sorting by it


===================
42.0.1 - 2024-11-21
===================
Expand Down
6 changes: 6 additions & 0 deletions resolwe/flow/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ class Meta(BaseResolweFilter.Meta):
model = DescriptorSchema


class CharInFilter(filters.BaseInFilter, filters.CharFilter):
"""Basic filter for CharField with 'in' lookup."""


class BaseCollectionFilter(TextFilterMixin, UserFilterMixin, BaseResolweFilter):
"""Base filter for Collection and Entity endpoints."""

Expand All @@ -299,6 +303,8 @@ class BaseCollectionFilter(TextFilterMixin, UserFilterMixin, BaseResolweFilter):
permission = filters.CharFilter(method="filter_for_user")
tags = TagsFilter()
text = filters.CharFilter(field_name="search", method="filter_text")
status = filters.CharFilter(field_name="status")
status__in = CharInFilter(field_name="status", lookup_expr="in")

class Meta(BaseResolweFilter.Meta):
"""Filter configuration."""
Expand Down
43 changes: 2 additions & 41 deletions resolwe/flow/serializers/collection.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Resolwe collection serializer."""

import logging
from typing import Optional

from rest_framework import serializers

from resolwe.flow.models import Collection, Data, DescriptorSchema
from resolwe.flow.models import Collection, DescriptorSchema
from resolwe.rest.fields import ProjectableJSONField

from .base import ResolweBaseSerializer
Expand All @@ -20,7 +19,7 @@ class BaseCollectionSerializer(ResolweBaseSerializer):

settings = ProjectableJSONField(required=False)
data_count = serializers.SerializerMethodField(required=False)
status = serializers.SerializerMethodField(required=False)
status = serializers.CharField(read_only=True)

def get_data_count(self, collection: Collection) -> int:
"""Return number of data objects on the collection."""
Expand All @@ -32,44 +31,6 @@ def get_data_count(self, collection: Collection) -> int:
else collection.data.count()
)

def get_status(self, collection: Collection) -> Optional[str]:
"""Return status of the collection based on the status of data objects.
When collection contains no data objects None is returned.
"""

status_order = [
Data.STATUS_ERROR,
Data.STATUS_UPLOADING,
Data.STATUS_PROCESSING,
Data.STATUS_PREPARING,
Data.STATUS_WAITING,
Data.STATUS_RESOLVING,
Data.STATUS_DONE,
]

# Use 'data_statuses' attribute when available. It is created in the
# BaseCollectionViewSet class. It contains all the distinct statuses of the
# data objects in the collection.
status_set = (
set(collection.data_statuses)
if hasattr(collection, "data_statuses")
else collection.data.values_list("status", flat=True).distinct()
)

if not status_set:
return None

for status in status_order:
if status in status_set:
return status

logger.warning(
"Could not determine the status of a collection.",
extra={"collection": collection.__dict__},
)
return None

class Meta:
"""CollectionSerializer Meta options."""

Expand Down
145 changes: 142 additions & 3 deletions resolwe/flow/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,26 @@ def test_collection_status(self):
)
self.assertEqual(get_collection(collections, "empty")["status"], None)

# Filter by status
response = self.client.get(self.list_url, {"status": "OK"})
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "done")

response = self.client.get(self.list_url, {"status": "ER"})
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "error")

response = self.client.get(
self.list_url,
{"status__in": f"{Data.STATUS_RESOLVING},{Data.STATUS_WAITING}"},
format="json",
)
self.assertEqual(len(response.data), 2)
self.assertCountEqual(
[response.data[0]["name"], response.data[1]["name"]],
["resolving", "waiting"],
)


class TestCollectionViewSetCaseDelete(
TestCollectionViewSetCaseCommonMixin, TransactionTestCase
Expand Down Expand Up @@ -1666,17 +1686,21 @@ def setUp(self):
"resolwe-api:entity-detail", kwargs={"pk": pk}
)

def _create_data(self):
def _create_data(self, data_status=None):
process = Process.objects.create(
name="Test process",
contributor=self.contributor,
)

return Data.objects.create(
data = Data.objects.create(
name="Test data",
contributor=self.contributor,
process=process,
)
if data_status:
data.status = data_status
data.save()

return data


class EntityViewSetTestTransaction(EntityViewSetTestCommonMixin, TransactionTestCase):
Expand Down Expand Up @@ -1765,6 +1789,121 @@ def test_prefetch(self):
self.assertEqual(len(response.data), 10)
self.assertIn(len(captured_queries), [7, 8])

def test_entity_status(self):
data_error = self._create_data(Data.STATUS_ERROR)
data_uploading = self._create_data(Data.STATUS_UPLOADING)
data_processing = self._create_data(Data.STATUS_PROCESSING)
data_preparing = self._create_data(Data.STATUS_PREPARING)
data_waiting = self._create_data(Data.STATUS_WAITING)
data_resolving = self._create_data(Data.STATUS_RESOLVING)
data_done = self._create_data(Data.STATUS_DONE)

entity = Entity.objects.create(contributor=self.contributor, name="error")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(
data_error,
data_uploading,
data_processing,
data_preparing,
data_waiting,
data_resolving,
data_done,
)

entity = Entity.objects.create(contributor=self.contributor, name="uploading")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(
data_uploading,
data_processing,
data_preparing,
data_waiting,
data_resolving,
data_done,
)

entity = Entity.objects.create(contributor=self.contributor, name="processing")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(
data_processing, data_preparing, data_waiting, data_resolving, data_done
)

entity = Entity.objects.create(contributor=self.contributor, name="preparing")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(data_preparing, data_waiting, data_resolving, data_done)

entity = Entity.objects.create(contributor=self.contributor, name="waiting")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(data_waiting, data_resolving, data_done)

entity = Entity.objects.create(contributor=self.contributor, name="resolving")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(data_resolving, data_done)

entity = Entity.objects.create(contributor=self.contributor, name="done")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add(data_done)

entity = Entity.objects.create(contributor=self.contributor, name="empty")
entity.set_permission(Permission.VIEW, get_anonymous_user())
entity.data.add()

request = factory.get("/", {}, format="json")
force_authenticate(request, self.contributor)
entities = self.entity_list_viewset(request).data

# entities = self.client.get(self.list_url).data

get_entity = lambda collections, name: next(
x for x in collections if x["name"] == name
)
self.assertEqual(get_entity(entities, "error")["status"], Data.STATUS_ERROR)
self.assertEqual(
get_entity(entities, "uploading")["status"], Data.STATUS_UPLOADING
)
self.assertEqual(
get_entity(entities, "processing")["status"], Data.STATUS_PROCESSING
)
self.assertEqual(
get_entity(entities, "preparing")["status"], Data.STATUS_PREPARING
)
self.assertEqual(get_entity(entities, "waiting")["status"], Data.STATUS_WAITING)
self.assertEqual(
get_entity(entities, "resolving")["status"], Data.STATUS_RESOLVING
)
self.assertEqual(get_entity(entities, "done")["status"], Data.STATUS_DONE)
self.assertEqual(get_entity(entities, "empty")["status"], None)

# Filter by status
request = factory.get(
"/", {"status": "OK", "collection__isnull": True}, format="json"
)
force_authenticate(request, self.contributor)
response = self.entity_list_viewset(request)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "done")

request = factory.get(
"/", {"status": "ER", "collection__isnull": True}, format="json"
)
response = self.entity_list_viewset(request)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "error")

request = factory.get(
"/",
{
"status__in": f"{Data.STATUS_RESOLVING},{Data.STATUS_WAITING}",
"collection__isnull": True,
},
format="json",
)
response = self.entity_list_viewset(request)
self.assertEqual(len(response.data), 2)
self.assertCountEqual(
[response.data[0]["name"], response.data[1]["name"]],
["resolving", "waiting"],
)

def test_list_filter_collection(self):
request = factory.get("/", {}, format="json")
force_authenticate(request, self.contributor)
Expand Down
6 changes: 6 additions & 0 deletions resolwe/flow/views/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from drf_spectacular.utils import extend_schema
from rest_framework import exceptions, mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response

from resolwe.flow.filters import CollectionFilter
Expand Down Expand Up @@ -91,11 +92,16 @@ class BaseCollectionViewSet(
"id",
"modified",
"name",
"status",
)
ordering = "id"

def get_queryset(self):
"""Prefetch permissions for current user."""
# Only annotate the queryset with status on safe methods. When updating
# the annotation interfers with update (as it adds group by statement).
if self.request.method in SAFE_METHODS:
self.queryset = self.queryset.annotate_status()
return self.prefetch_current_user_permissions(self.queryset)

def create(self, request, *args, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions resolwe/flow/views/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class EntityViewSet(ObservableMixin, BaseCollectionViewSet):
)
.annotate(data_statuses=Subquery(data_status_subquery))
.annotate(data_count=Subquery(data_count_subquery))
.annotate_status()
)

def order_queryset(self, queryset):
Expand Down

0 comments on commit 5021482

Please sign in to comment.