Skip to content

Commit d55b0e5

Browse files
author
Gianluca Lefebvre
committed
Add filtering and ordering to secrets list API
1 parent 64d7866 commit d55b0e5

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import django_filters
2+
from django_filters import rest_framework as filters
3+
4+
from teamvault.apps.secrets.api.serializers import (
5+
REPR_ACCESS_POLICY,
6+
REPR_CONTENT_TYPE,
7+
SECRET_REPR_STATUS,
8+
)
9+
from teamvault.apps.secrets.enums import SecretStatus
10+
from teamvault.apps.secrets.models import Secret
11+
12+
# Deleted secrets are never returned by the list queryset, so they are not an
13+
# allowed filter value.
14+
SELECTABLE_STATUS = {repr_: value for repr_, value in SECRET_REPR_STATUS.items() if value != SecretStatus.DELETED}
15+
16+
17+
def _string_choice_filter(repr_to_value):
18+
"""ChoiceFilter accepting the API's string reprs (e.g. ``needs_changing``)
19+
and coercing them to the underlying integer field value.
20+
21+
Invariant: ``choices`` and the coercion are both derived from
22+
``repr_to_value``, so every value that passes choice-validation is
23+
guaranteed to be a key here. Callers MUST pass a single dict whose keys
24+
are exactly the valid API reprs — do not supply choices and coercion
25+
from different sources, or coercion can KeyError → HTTP 500.
26+
"""
27+
return django_filters.TypedChoiceFilter(
28+
choices=[(repr_, repr_) for repr_ in repr_to_value],
29+
coerce=lambda value: repr_to_value[value],
30+
)
31+
32+
33+
class SecretListFilter(filters.FilterSet):
34+
access_policy = _string_choice_filter(REPR_ACCESS_POLICY)
35+
content_type = _string_choice_filter(REPR_CONTENT_TYPE)
36+
status = _string_choice_filter(SELECTABLE_STATUS)
37+
38+
name = django_filters.CharFilter(lookup_expr='icontains')
39+
url = django_filters.CharFilter(lookup_expr='icontains')
40+
username = django_filters.CharFilter(lookup_expr='icontains')
41+
created_by = django_filters.CharFilter(
42+
field_name='created_by__username',
43+
lookup_expr='icontains',
44+
)
45+
46+
ordering = django_filters.OrderingFilter(
47+
fields=(
48+
('last_read', 'last_read'),
49+
('last_changed', 'last_changed'),
50+
('created', 'created'),
51+
('name', 'name'),
52+
),
53+
)
54+
55+
@property
56+
def qs(self):
57+
parent = super().qs
58+
# Guarantee a deterministic total order so LIMIT/OFFSET pagination
59+
# is stable across requests even when the sort key has ties.
60+
return parent.order_by(*parent.query.order_by, 'pk')
61+
62+
class Meta:
63+
model = Secret
64+
fields = ('needs_changing_on_leave',)

teamvault/apps/secrets/api/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.db.models import Max
66
from django.shortcuts import get_object_or_404
77
from django.utils.translation import gettext_lazy as _
8+
from django_filters import rest_framework as filters
89
from rest_framework import generics, status
910
from rest_framework.decorators import api_view
1011
from rest_framework.exceptions import PermissionDenied
@@ -17,6 +18,7 @@
1718
from teamvault.apps.secrets.enums import ContentType, SecretStatus
1819
from teamvault.apps.secrets.exceptions import PermissionError as SecretPermissionError
1920
from teamvault.apps.secrets.services.revision import RevisionService
21+
from .filters import SecretListFilter
2022
from .serializers import (
2123
PendingSecretSerializer,
2224
SecretDetailSerializer,
@@ -54,6 +56,8 @@ def perform_update(self, serializer):
5456
class SecretList(generics.ListCreateAPIView):
5557
model = Secret
5658
serializer_class = SecretSerializer
59+
filter_backends = (filters.DjangoFilterBackend,)
60+
filterset_class = SecretListFilter
5761

5862
def get_queryset(self):
5963
if 'search' in self.request.query_params:

0 commit comments

Comments
 (0)