prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Liked photos API endpoint
  To add an overview of liked photos to ThaliApp, we need a new endpoint for liked photos.
  
  I think it would be best to have `api/v2/photos/photos/` with `liked` boolean GET filter. It will need to do some filtering to prevent photos that are not published in an album from being returned.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `website/photos/api/v2/urls.py`
  Content:
  ```
  1 """Photos app API v2 urls."""
  2 from django.urls import include, path
  3 
  4 from photos.api.v2.views import AlbumDetailView, AlbumListView, PhotoLikeView
  5 
  6 app_name = "photos"
  7 
  8 urlpatterns = [
  9     path(
  10         "photos/",
  11         include(
  12             [
  13                 path("albums/", AlbumListView.as_view(), name="album-list"),
  14                 path(
  15                     "albums/<slug:slug>/",
  16                     AlbumDetailView.as_view(),
  17                     name="album-detail",
  18                 ),
  19                 path(
  20                     "photos/<int:pk>/like/", PhotoLikeView.as_view(), name="photo-like"
  21                 ),
  22             ]
  23         ),
  24     ),
  25 ]
  26
  ```
  
  Path: `website/photos/api/v2/views.py`
  Content:
  ```
  1 from django.db.models import Count, Prefetch, Q
  2 
  3 from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
  4 from rest_framework import filters, status
  5 from rest_framework.exceptions import PermissionDenied
  6 from rest_framework.generics import ListAPIView, RetrieveAPIView
  7 from rest_framework.response import Response
  8 from rest_framework.views import APIView
  9 
  10 from photos import services
  11 from photos.api.v2.serializers.album import AlbumListSerializer, AlbumSerializer
  12 from photos.models import Album, Like, Photo
  13 
  14 
  15 class AlbumListView(ListAPIView):
  16     """Returns an overview of all albums."""
  17 
  18     serializer_class = AlbumListSerializer
  19     queryset = Album.objects.filter(hidden=False)
  20     permission_classes = [
  21         IsAuthenticatedOrTokenHasScope,
  22     ]
  23     required_scopes = ["photos:read"]
  24     filter_backends = (filters.SearchFilter,)
  25     search_fields = ("title", "date", "slug")
  26 
  27 
  28 class AlbumDetailView(RetrieveAPIView):
  29     """Returns the details of an album."""
  30 
  31     serializer_class = AlbumSerializer
  32     permission_classes = [
  33         IsAuthenticatedOrTokenHasScope,
  34     ]
  35     required_scopes = ["photos:read"]
  36     lookup_field = "slug"
  37 
  38     def retrieve(self, request, *args, **kwargs):
  39         if not services.is_album_accessible(request, self.get_object()):
  40             raise PermissionDenied
  41         return super().retrieve(request, *args, **kwargs)
  42 
  43     def get_queryset(self):
  44         photos = Photo.objects.select_properties("num_likes")
  45         if self.request.member:
  46             photos = photos.annotate(
  47                 member_likes=Count("likes", filter=Q(likes__member=self.request.member))
  48             )
  49         return Album.objects.filter(hidden=False).prefetch_related(
  50             Prefetch("photo_set", queryset=photos)
  51         )
  52 
  53 
  54 class PhotoLikeView(APIView):
  55     permission_classes = [IsAuthenticatedOrTokenHasScope]
  56     required_scopes = ["photos:read"]
  57 
  58     def get(self, request, **kwargs):
  59         photo_id = kwargs.get("pk")
  60         try:
  61             photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)
  62         except Photo.DoesNotExist:
  63             return Response(status=status.HTTP_404_NOT_FOUND)
  64 
  65         return Response(
  66             {
  67                 "liked": photo.likes.filter(member=request.member).exists(),
  68                 "num_likes": photo.num_likes,
  69             },
  70             status=status.HTTP_200_OK,
  71         )
  72 
  73     def post(self, request, **kwargs):
  74         photo_id = kwargs.get("pk")
  75         try:
  76             photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)
  77         except Photo.DoesNotExist:
  78             return Response(status=status.HTTP_404_NOT_FOUND)
  79 
  80         _, created = Like.objects.get_or_create(photo=photo, member=request.member)
  81 
  82         if created:
  83             return Response(
  84                 {
  85                     "liked": photo.likes.filter(member=request.member).exists(),
  86                     "num_likes": photo.num_likes,
  87                 },
  88                 status=status.HTTP_201_CREATED,
  89             )
  90         return Response(
  91             {
  92                 "liked": photo.likes.filter(member=request.member).exists(),
  93                 "num_likes": photo.num_likes,
  94             },
  95             status=status.HTTP_200_OK,
  96         )
  97 
  98     def delete(self, request, **kwargs):
  99         photo_id = kwargs.get("pk")
  100         try:
  101             photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)
  102         except Photo.DoesNotExist:
  103             return Response(status=status.HTTP_404_NOT_FOUND)
  104 
  105         try:
  106             like = Like.objects.filter(photo__album__hidden=False).get(
  107                 member=request.member, photo__pk=photo_id
  108             )
  109         except Like.DoesNotExist:
  110             return Response(
  111                 {
  112                     "liked": False,
  113                     "num_likes": photo.num_likes,
  114                 },
  115                 status=status.HTTP_204_NO_CONTENT,
  116             )
  117 
  118         like.delete()
  119 
  120         return Response(
  121             {
  122                 "liked": False,
  123                 "num_likes": photo.num_likes,
  124             },
  125             status=status.HTTP_202_ACCEPTED,
  126         )
  127
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/website/photos/api/v2/urls.py b/website/photos/api/v2/urls.py
  --- a/website/photos/api/v2/urls.py
  +++ b/website/photos/api/v2/urls.py
  @@ -1,7 +1,12 @@
   """Photos app API v2 urls."""
   from django.urls import include, path
   
  -from photos.api.v2.views import AlbumDetailView, AlbumListView, PhotoLikeView
  +from photos.api.v2.views import (
  +    AlbumDetailView,
  +    AlbumListView,
  +    LikedPhotosListView,
  +    PhotoLikeView,
  +)
   
   app_name = "photos"
   
  @@ -19,6 +24,9 @@
                   path(
                       "photos/<int:pk>/like/", PhotoLikeView.as_view(), name="photo-like"
                   ),
  +                path(
  +                    "photos/liked/", LikedPhotosListView.as_view(), name="liked-photos"
  +                ),
               ]
           ),
       ),
  diff --git a/website/photos/api/v2/views.py b/website/photos/api/v2/views.py
  --- a/website/photos/api/v2/views.py
  +++ b/website/photos/api/v2/views.py
  @@ -8,7 +8,11 @@
   from rest_framework.views import APIView
   
   from photos import services
  -from photos.api.v2.serializers.album import AlbumListSerializer, AlbumSerializer
  +from photos.api.v2.serializers.album import (
  +    AlbumListSerializer,
  +    AlbumSerializer,
  +    PhotoListSerializer,
  +)
   from photos.models import Album, Like, Photo
   
   
  @@ -51,6 +55,35 @@
           )
   
   
  +class LikedPhotosListView(ListAPIView):
  +    """Returns the details the liked album."""
  +
  +    serializer_class = PhotoListSerializer
  +    permission_classes = [
  +        IsAuthenticatedOrTokenHasScope,
  +    ]
  +    required_scopes = ["photos:read"]
  +
  +    def get(self, request, *args, **kwargs):
  +        if not self.request.member:
  +            return Response(
  +                data={
  +                    "detail": "You need to be a member in order to view your liked photos."
  +                },
  +                status=status.HTTP_403_FORBIDDEN,
  +            )
  +        return self.list(request, *args, **kwargs)
  +
  +    def get_queryset(self):
  +        return (
  +            Photo.objects.filter(likes__member=self.request.member, album__hidden=False)
  +            .annotate(
  +                member_likes=Count("likes", filter=Q(likes__member=self.request.member))
  +            )
  +            .select_properties("num_likes")
  +        )
  +
  +
   class PhotoLikeView(APIView):
       permission_classes = [IsAuthenticatedOrTokenHasScope]
       required_scopes = ["photos:read"]
  
verification_info: {"golden_diff": "diff --git a/website/photos/api/v2/urls.py b/website/photos/api/v2/urls.py\n--- a/website/photos/api/v2/urls.py\n+++ b/website/photos/api/v2/urls.py\n@@ -1,7 +1,12 @@\n \"\"\"Photos app API v2 urls.\"\"\"\n from django.urls import include, path\n \n-from photos.api.v2.views import AlbumDetailView, AlbumListView, PhotoLikeView\n+from photos.api.v2.views import (\n+    AlbumDetailView,\n+    AlbumListView,\n+    LikedPhotosListView,\n+    PhotoLikeView,\n+)\n \n app_name = \"photos\"\n \n@@ -19,6 +24,9 @@\n                 path(\n                     \"photos/<int:pk>/like/\", PhotoLikeView.as_view(), name=\"photo-like\"\n                 ),\n+                path(\n+                    \"photos/liked/\", LikedPhotosListView.as_view(), name=\"liked-photos\"\n+                ),\n             ]\n         ),\n     ),\ndiff --git a/website/photos/api/v2/views.py b/website/photos/api/v2/views.py\n--- a/website/photos/api/v2/views.py\n+++ b/website/photos/api/v2/views.py\n@@ -8,7 +8,11 @@\n from rest_framework.views import APIView\n \n from photos import services\n-from photos.api.v2.serializers.album import AlbumListSerializer, AlbumSerializer\n+from photos.api.v2.serializers.album import (\n+    AlbumListSerializer,\n+    AlbumSerializer,\n+    PhotoListSerializer,\n+)\n from photos.models import Album, Like, Photo\n \n \n@@ -51,6 +55,35 @@\n         )\n \n \n+class LikedPhotosListView(ListAPIView):\n+    \"\"\"Returns the details the liked album.\"\"\"\n+\n+    serializer_class = PhotoListSerializer\n+    permission_classes = [\n+        IsAuthenticatedOrTokenHasScope,\n+    ]\n+    required_scopes = [\"photos:read\"]\n+\n+    def get(self, request, *args, **kwargs):\n+        if not self.request.member:\n+            return Response(\n+                data={\n+                    \"detail\": \"You need to be a member in order to view your liked photos.\"\n+                },\n+                status=status.HTTP_403_FORBIDDEN,\n+            )\n+        return self.list(request, *args, **kwargs)\n+\n+    def get_queryset(self):\n+        return (\n+            Photo.objects.filter(likes__member=self.request.member, album__hidden=False)\n+            .annotate(\n+                member_likes=Count(\"likes\", filter=Q(likes__member=self.request.member))\n+            )\n+            .select_properties(\"num_likes\")\n+        )\n+\n+\n class PhotoLikeView(APIView):\n     permission_classes = [IsAuthenticatedOrTokenHasScope]\n     required_scopes = [\"photos:read\"]\n", "issue": "Liked photos API endpoint\nTo add an overview of liked photos to ThaliApp, we need a new endpoint for liked photos.\r\n\r\nI think it would be best to have `api/v2/photos/photos/` with `liked` boolean GET filter. It will need to do some filtering to prevent photos that are not published in an album from being returned.\n", "before_files": [{"content": "\"\"\"Photos app API v2 urls.\"\"\"\nfrom django.urls import include, path\n\nfrom photos.api.v2.views import AlbumDetailView, AlbumListView, PhotoLikeView\n\napp_name = \"photos\"\n\nurlpatterns = [\n    path(\n        \"photos/\",\n        include(\n            [\n                path(\"albums/\", AlbumListView.as_view(), name=\"album-list\"),\n                path(\n                    \"albums/<slug:slug>/\",\n                    AlbumDetailView.as_view(),\n                    name=\"album-detail\",\n                ),\n                path(\n                    \"photos/<int:pk>/like/\", PhotoLikeView.as_view(), name=\"photo-like\"\n                ),\n            ]\n        ),\n    ),\n]\n", "path": "website/photos/api/v2/urls.py"}, {"content": "from django.db.models import Count, Prefetch, Q\n\nfrom oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope\nfrom rest_framework import filters, status\nfrom rest_framework.exceptions import PermissionDenied\nfrom rest_framework.generics import ListAPIView, RetrieveAPIView\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom photos import services\nfrom photos.api.v2.serializers.album import AlbumListSerializer, AlbumSerializer\nfrom photos.models import Album, Like, Photo\n\n\nclass AlbumListView(ListAPIView):\n    \"\"\"Returns an overview of all albums.\"\"\"\n\n    serializer_class = AlbumListSerializer\n    queryset = Album.objects.filter(hidden=False)\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"photos:read\"]\n    filter_backends = (filters.SearchFilter,)\n    search_fields = (\"title\", \"date\", \"slug\")\n\n\nclass AlbumDetailView(RetrieveAPIView):\n    \"\"\"Returns the details of an album.\"\"\"\n\n    serializer_class = AlbumSerializer\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"photos:read\"]\n    lookup_field = \"slug\"\n\n    def retrieve(self, request, *args, **kwargs):\n        if not services.is_album_accessible(request, self.get_object()):\n            raise PermissionDenied\n        return super().retrieve(request, *args, **kwargs)\n\n    def get_queryset(self):\n        photos = Photo.objects.select_properties(\"num_likes\")\n        if self.request.member:\n            photos = photos.annotate(\n                member_likes=Count(\"likes\", filter=Q(likes__member=self.request.member))\n            )\n        return Album.objects.filter(hidden=False).prefetch_related(\n            Prefetch(\"photo_set\", queryset=photos)\n        )\n\n\nclass PhotoLikeView(APIView):\n    permission_classes = [IsAuthenticatedOrTokenHasScope]\n    required_scopes = [\"photos:read\"]\n\n    def get(self, request, **kwargs):\n        photo_id = kwargs.get(\"pk\")\n        try:\n            photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)\n        except Photo.DoesNotExist:\n            return Response(status=status.HTTP_404_NOT_FOUND)\n\n        return Response(\n            {\n                \"liked\": photo.likes.filter(member=request.member).exists(),\n                \"num_likes\": photo.num_likes,\n            },\n            status=status.HTTP_200_OK,\n        )\n\n    def post(self, request, **kwargs):\n        photo_id = kwargs.get(\"pk\")\n        try:\n            photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)\n        except Photo.DoesNotExist:\n            return Response(status=status.HTTP_404_NOT_FOUND)\n\n        _, created = Like.objects.get_or_create(photo=photo, member=request.member)\n\n        if created:\n            return Response(\n                {\n                    \"liked\": photo.likes.filter(member=request.member).exists(),\n                    \"num_likes\": photo.num_likes,\n                },\n                status=status.HTTP_201_CREATED,\n            )\n        return Response(\n            {\n                \"liked\": photo.likes.filter(member=request.member).exists(),\n                \"num_likes\": photo.num_likes,\n            },\n            status=status.HTTP_200_OK,\n        )\n\n    def delete(self, request, **kwargs):\n        photo_id = kwargs.get(\"pk\")\n        try:\n            photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)\n        except Photo.DoesNotExist:\n            return Response(status=status.HTTP_404_NOT_FOUND)\n\n        try:\n            like = Like.objects.filter(photo__album__hidden=False).get(\n                member=request.member, photo__pk=photo_id\n            )\n        except Like.DoesNotExist:\n            return Response(\n                {\n                    \"liked\": False,\n                    \"num_likes\": photo.num_likes,\n                },\n                status=status.HTTP_204_NO_CONTENT,\n            )\n\n        like.delete()\n\n        return Response(\n            {\n                \"liked\": False,\n                \"num_likes\": photo.num_likes,\n            },\n            status=status.HTTP_202_ACCEPTED,\n        )\n", "path": "website/photos/api/v2/views.py"}], "after_files": [{"content": "\"\"\"Photos app API v2 urls.\"\"\"\nfrom django.urls import include, path\n\nfrom photos.api.v2.views import (\n    AlbumDetailView,\n    AlbumListView,\n    LikedPhotosListView,\n    PhotoLikeView,\n)\n\napp_name = \"photos\"\n\nurlpatterns = [\n    path(\n        \"photos/\",\n        include(\n            [\n                path(\"albums/\", AlbumListView.as_view(), name=\"album-list\"),\n                path(\n                    \"albums/<slug:slug>/\",\n                    AlbumDetailView.as_view(),\n                    name=\"album-detail\",\n                ),\n                path(\n                    \"photos/<int:pk>/like/\", PhotoLikeView.as_view(), name=\"photo-like\"\n                ),\n                path(\n                    \"photos/liked/\", LikedPhotosListView.as_view(), name=\"liked-photos\"\n                ),\n            ]\n        ),\n    ),\n]\n", "path": "website/photos/api/v2/urls.py"}, {"content": "from django.db.models import Count, Prefetch, Q\n\nfrom oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope\nfrom rest_framework import filters, status\nfrom rest_framework.exceptions import PermissionDenied\nfrom rest_framework.generics import ListAPIView, RetrieveAPIView\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom photos import services\nfrom photos.api.v2.serializers.album import (\n    AlbumListSerializer,\n    AlbumSerializer,\n    PhotoListSerializer,\n)\nfrom photos.models import Album, Like, Photo\n\n\nclass AlbumListView(ListAPIView):\n    \"\"\"Returns an overview of all albums.\"\"\"\n\n    serializer_class = AlbumListSerializer\n    queryset = Album.objects.filter(hidden=False)\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"photos:read\"]\n    filter_backends = (filters.SearchFilter,)\n    search_fields = (\"title\", \"date\", \"slug\")\n\n\nclass AlbumDetailView(RetrieveAPIView):\n    \"\"\"Returns the details of an album.\"\"\"\n\n    serializer_class = AlbumSerializer\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"photos:read\"]\n    lookup_field = \"slug\"\n\n    def retrieve(self, request, *args, **kwargs):\n        if not services.is_album_accessible(request, self.get_object()):\n            raise PermissionDenied\n        return super().retrieve(request, *args, **kwargs)\n\n    def get_queryset(self):\n        photos = Photo.objects.select_properties(\"num_likes\")\n        if self.request.member:\n            photos = photos.annotate(\n                member_likes=Count(\"likes\", filter=Q(likes__member=self.request.member))\n            )\n        return Album.objects.filter(hidden=False).prefetch_related(\n            Prefetch(\"photo_set\", queryset=photos)\n        )\n\n\nclass LikedPhotosListView(ListAPIView):\n    \"\"\"Returns the details the liked album.\"\"\"\n\n    serializer_class = PhotoListSerializer\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"photos:read\"]\n\n    def get(self, request, *args, **kwargs):\n        if not self.request.member:\n            return Response(\n                data={\n                    \"detail\": \"You need to be a member in order to view your liked photos.\"\n                },\n                status=status.HTTP_403_FORBIDDEN,\n            )\n        return self.list(request, *args, **kwargs)\n\n    def get_queryset(self):\n        return (\n            Photo.objects.filter(likes__member=self.request.member, album__hidden=False)\n            .annotate(\n                member_likes=Count(\"likes\", filter=Q(likes__member=self.request.member))\n            )\n            .select_properties(\"num_likes\")\n        )\n\n\nclass PhotoLikeView(APIView):\n    permission_classes = [IsAuthenticatedOrTokenHasScope]\n    required_scopes = [\"photos:read\"]\n\n    def get(self, request, **kwargs):\n        photo_id = kwargs.get(\"pk\")\n        try:\n            photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)\n        except Photo.DoesNotExist:\n            return Response(status=status.HTTP_404_NOT_FOUND)\n\n        return Response(\n            {\n                \"liked\": photo.likes.filter(member=request.member).exists(),\n                \"num_likes\": photo.num_likes,\n            },\n            status=status.HTTP_200_OK,\n        )\n\n    def post(self, request, **kwargs):\n        photo_id = kwargs.get(\"pk\")\n        try:\n            photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)\n        except Photo.DoesNotExist:\n            return Response(status=status.HTTP_404_NOT_FOUND)\n\n        _, created = Like.objects.get_or_create(photo=photo, member=request.member)\n\n        if created:\n            return Response(\n                {\n                    \"liked\": photo.likes.filter(member=request.member).exists(),\n                    \"num_likes\": photo.num_likes,\n                },\n                status=status.HTTP_201_CREATED,\n            )\n        return Response(\n            {\n                \"liked\": photo.likes.filter(member=request.member).exists(),\n                \"num_likes\": photo.num_likes,\n            },\n            status=status.HTTP_200_OK,\n        )\n\n    def delete(self, request, **kwargs):\n        photo_id = kwargs.get(\"pk\")\n        try:\n            photo = Photo.objects.filter(album__hidden=False).get(pk=photo_id)\n        except Photo.DoesNotExist:\n            return Response(status=status.HTTP_404_NOT_FOUND)\n\n        try:\n            like = Like.objects.filter(photo__album__hidden=False).get(\n                member=request.member, photo__pk=photo_id\n            )\n        except Like.DoesNotExist:\n            return Response(\n                {\n                    \"liked\": False,\n                    \"num_likes\": photo.num_likes,\n                },\n                status=status.HTTP_204_NO_CONTENT,\n            )\n\n        like.delete()\n\n        return Response(\n            {\n                \"liked\": False,\n                \"num_likes\": photo.num_likes,\n            },\n            status=status.HTTP_202_ACCEPTED,\n        )\n", "path": "website/photos/api/v2/views.py"}]}