Skip to content

Commit

Permalink
Add facedetection api endpoints (#3218)
Browse files Browse the repository at this point in the history
* Add reference face API

* Add /photos/facedetection/matches

* Deduplicate getting user's photos

* Update website/facedetection/api/v2/views.py

Co-authored-by: Ties Dirksen <[email protected]>

---------

Co-authored-by: Dirk Doesburg <[email protected]>
Co-authored-by: Ties Dirksen <[email protected]>
  • Loading branch information
3 people authored May 23, 2024
1 parent ac86c00 commit a69196b
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 23 deletions.
Empty file.
20 changes: 20 additions & 0 deletions website/facedetection/api/v2/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from facedetection.models import ReferenceFace
from thaliawebsite.api.v2.serializers.cleaned_model_serializer import (
CleanedModelSerializer,
)
from thaliawebsite.api.v2.serializers.thumbnail import ThumbnailSerializer


class ReferenceFaceSerializer(CleanedModelSerializer):
class Meta:
model = ReferenceFace
fields = (
"pk",
"status",
"created_at",
"file",
)

read_only_fields = ("status",)

file = ThumbnailSerializer()
24 changes: 24 additions & 0 deletions website/facedetection/api/v2/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Events app calendarjs API urls."""
from django.urls import path

from .views import ReferenceFaceDeleteView, ReferenceFaceListView, YourPhotosView

app_name = "facedetection"

urlpatterns = [
path(
"photos/facedetection/matches/",
YourPhotosView.as_view(),
name="your-photos",
),
path(
"photos/facedetection/reference-faces/",
ReferenceFaceListView.as_view(),
name="reference-faces",
),
path(
"photos/facedetection/reference-faces/<int:pk>/",
ReferenceFaceDeleteView.as_view(),
name="reference-faces-delete",
),
]
94 changes: 94 additions & 0 deletions website/facedetection/api/v2/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from django.conf import settings
from django.utils import timezone

from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import DestroyAPIView, ListAPIView, ListCreateAPIView
from rest_framework.schemas.openapi import AutoSchema

from facedetection.services import get_user_photos
from photos.api.v2.serializers.photo import PhotoListSerializer
from thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod
from utils.media.services import fetch_thumbnails

from .serializers import ReferenceFaceSerializer


class YourPhotosView(ListAPIView):
serializer_class = PhotoListSerializer
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ["photos:read", "facedetection:read"]

schema = AutoSchema(operation_id_base="FacedetectionMatches")

def get(self, request, *args, **kwargs):
if not request.member or request.member.current_membership is None:
raise PermissionDenied(
detail="You need to be a member in order to view your facedetection photos."
)
return self.list(request, *args, **kwargs)

def get_serializer(self, *args, **kwargs):
if len(args) > 0:
photos = args[0]
fetch_thumbnails([photo.file for photo in photos])
return super().get_serializer(*args, **kwargs)

def get_queryset(self):
return get_user_photos(self.request.member)


class ReferenceFaceListView(ListCreateAPIView):
serializer_class = ReferenceFaceSerializer
permission_classes = [
IsAuthenticatedOrTokenHasScopeForMethod,
]
required_scopes_per_method = {
"GET": ["facedetection:read"],
"POST": ["facedetection:write"],
}

def get_serializer(self, *args, **kwargs):
if len(args) > 0:
reference_faces = args[0]
fetch_thumbnails([reference.file for reference in reference_faces])
return super().get_serializer(*args, **kwargs)

def create(self, request, *args, **kwargs):
if request.member.current_membership is None:
raise PermissionDenied(
detail="You need to be a member to use this feature."
)
if (
request.member.reference_faces.filter(
marked_for_deletion_at__isnull=True,
).count()
>= settings.FACEDETECTION_MAX_NUM_REFERENCE_FACES
):
raise PermissionDenied(
detail="You have reached the maximum number of reference faces."
)
return super().create(request, *args, **kwargs)

def perform_create(self, serializer):
serializer.save(user=self.request.member)

def get_queryset(self):
return self.request.member.reference_faces.filter(
marked_for_deletion_at__isnull=True
).all()


class ReferenceFaceDeleteView(DestroyAPIView):
serializer_class = ReferenceFaceSerializer
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ["facedetection:write"]

def get_queryset(self):
return self.request.member.reference_faces.filter(
marked_for_deletion_at__isnull=True
).all()

def perform_destroy(self, instance):
instance.marked_for_deletion_at = timezone.now()
instance.save()
28 changes: 27 additions & 1 deletion website/facedetection/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
import logging

from django.conf import settings
from django.db.models import Q
from django.db.models import Count, Q
from django.utils import timezone

import boto3
from sentry_sdk import capture_exception

from members.models.member import Member
from photos.models import Photo
from utils.media.services import get_media_url

Expand Down Expand Up @@ -193,3 +194,28 @@ def submit_new_photos() -> int:
count += len(photos)

return count


def get_user_photos(member: Member):
reference_faces = member.reference_faces.filter(
marked_for_deletion_at__isnull=True,
)

# Filter out matches from long before the member's first membership.
albums_since = member.earliest_membership.since - timezone.timedelta(days=31)
photos = Photo.objects.select_related("album").filter(album__date__gte=albums_since)

# Filter out matches from after the member's last membership.
if member.latest_membership.until is not None:
photos = photos.filter(album__date__lte=member.latest_membership.until)

# Actually match the reference faces.
photos = photos.filter(album__hidden=False, album__is_processing=False).filter(
facedetectionphoto__encodings__matches__reference__in=reference_faces,
)

return (
photos.annotate(member_likes=Count("likes", filter=Q(likes__member=member)))
.select_properties("num_likes")
.order_by("-album__date", "-pk")
)
24 changes: 2 additions & 22 deletions website/facedetection/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from .forms import ReferenceFaceUploadForm
from .models import ReferenceFace
from .services import get_user_photos


class YourPhotosView(LoginRequiredMixin, PagedView):
Expand All @@ -30,28 +31,7 @@ def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)

def get_queryset(self):
member = self.request.member

reference_faces = member.reference_faces.filter(
marked_for_deletion_at__isnull=True,
)

# Filter out matches from long before the member's first membership.
albums_since = member.earliest_membership.since - timezone.timedelta(days=31)
photos = Photo.objects.select_related("album").filter(
album__date__gte=albums_since
)

# Filter out matches from after the member's last membership.
if member.latest_membership.until is not None:
photos = photos.filter(album__date__lte=member.latest_membership.until)

# Actually match the reference faces.
photos = photos.filter(album__hidden=False, album__is_processing=False).filter(
facedetectionphoto__encodings__matches__reference__in=reference_faces,
)

return photos.select_properties("num_likes").order_by("-album__date", "-pk")
return get_user_photos(self.request.member)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Expand Down
1 change: 1 addition & 0 deletions website/thaliawebsite/api/v2/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
path("", include("pizzas.api.v2.urls")),
path("", include("pushnotifications.api.v2.urls")),
path("", include("sales.api.v2.urls")),
path("", include("facedetection.api.v2.urls")),
path("", include("thabloid.api.v2.urls")),
path(
"schema",
Expand Down
2 changes: 2 additions & 0 deletions website/thaliawebsite/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,8 @@ def show_toolbar(request):
"events:read": "Read access to events and your event registrations",
"events:register": "Write access to the state of your event registrations",
"events:admin": "Admin access to the events",
"facedetection:read": "Read access to facedetection",
"facedetection:write": "Write access to facedetection",
"food:read": "Read access to food events",
"food:order": "Order access to food events",
"food:admin": "Admin access to food events",
Expand Down

0 comments on commit a69196b

Please sign in to comment.