Collection Speed Improvements (#874)
* Add UltraSlimCollectionSerializer and update CollectionViewSet for optimized listing - Introduced UltraSlimCollectionSerializer for efficient data representation. - Updated CollectionViewSet to use the new serializer for list actions. - Enhanced queryset optimizations with prefetching for related images. - Modified frontend components to support SlimCollection type for better performance. * Optimize rendering of collection cards by adding a unique key to the each block
This commit is contained in:
@@ -655,4 +655,49 @@ class CollectionInviteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CollectionInvite
|
||||
fields = ['id', 'collection', 'created_at', 'name', 'collection_owner_username', 'collection_user_first_name', 'collection_user_last_name']
|
||||
read_only_fields = ['id', 'created_at']
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
class UltraSlimCollectionSerializer(serializers.ModelSerializer):
|
||||
location_images = serializers.SerializerMethodField()
|
||||
location_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = [
|
||||
'id', 'user', 'name', 'description', 'is_public', 'start_date', 'end_date',
|
||||
'is_archived', 'link', 'created_at', 'updated_at', 'location_images',
|
||||
'location_count', 'shared_with'
|
||||
]
|
||||
read_only_fields = fields # All fields are read-only for listing
|
||||
|
||||
def get_location_images(self, obj):
|
||||
"""Get primary images from locations in this collection, optimized with select_related"""
|
||||
# Filter first, then slice (removed slicing)
|
||||
images = ContentImage.objects.filter(
|
||||
location__collections=obj
|
||||
).select_related('user').prefetch_related('location')
|
||||
|
||||
return ContentImageSerializer(
|
||||
images,
|
||||
many=True,
|
||||
context={'request': self.context.get('request')}
|
||||
).data
|
||||
|
||||
def get_location_count(self, obj):
|
||||
"""Get count of locations in this collection"""
|
||||
# This uses the cached count if available, or does a simple count query
|
||||
return obj.locations.count()
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
|
||||
# make it show the uuid instead of the pk for the user
|
||||
representation['user'] = str(instance.user.uuid)
|
||||
|
||||
# Make it display the user uuid for the shared users instead of the PK
|
||||
shared_uuids = []
|
||||
for user in instance.shared_with.all():
|
||||
shared_uuids.append(str(user.uuid))
|
||||
representation['shared_with'] = shared_uuids
|
||||
return representation
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Prefetch
|
||||
from django.db.models.functions import Lower
|
||||
from django.db import transaction
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from adventures.models import Collection, Location, Transportation, Note, Checklist, CollectionInvite
|
||||
from adventures.models import Collection, Location, Transportation, Note, Checklist, CollectionInvite, ContentImage
|
||||
from adventures.permissions import CollectionShared
|
||||
from adventures.serializers import CollectionSerializer, CollectionInviteSerializer
|
||||
from adventures.serializers import CollectionSerializer, CollectionInviteSerializer, UltraSlimCollectionSerializer
|
||||
from users.models import CustomUser as User
|
||||
from adventures.utils import pagination
|
||||
from users.serializers import CustomUserDetailsSerializer as UserSerializer
|
||||
|
||||
|
||||
class CollectionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CollectionSerializer
|
||||
permission_classes = [CollectionShared]
|
||||
pagination_class = pagination.StandardResultsSetPagination
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return different serializers based on the action"""
|
||||
if self.action in ['list', 'all', 'archived', 'shared']:
|
||||
return UltraSlimCollectionSerializer
|
||||
return CollectionSerializer
|
||||
|
||||
def apply_sorting(self, queryset):
|
||||
order_by = self.request.query_params.get('order_by', 'name')
|
||||
order_direction = self.request.query_params.get('order_direction', 'asc')
|
||||
@@ -51,30 +58,89 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
"""Override to add nested and exclusion contexts based on query parameters"""
|
||||
context = super().get_serializer_context()
|
||||
|
||||
# Handle nested parameter
|
||||
is_nested = self.request.query_params.get('nested', 'false').lower() == 'true'
|
||||
if is_nested:
|
||||
context['nested'] = True
|
||||
|
||||
# Handle individual exclusion parameters (if using granular approach)
|
||||
exclude_params = [
|
||||
'exclude_transportations',
|
||||
'exclude_notes',
|
||||
'exclude_checklists',
|
||||
'exclude_lodging'
|
||||
]
|
||||
|
||||
for param in exclude_params:
|
||||
if self.request.query_params.get(param, 'false').lower() == 'true':
|
||||
context[param] = True
|
||||
# Handle nested parameter (only for full serializer actions)
|
||||
if self.action not in ['list', 'all', 'archived', 'shared']:
|
||||
is_nested = self.request.query_params.get('nested', 'false').lower() == 'true'
|
||||
if is_nested:
|
||||
context['nested'] = True
|
||||
|
||||
# Handle individual exclusion parameters (if using granular approach)
|
||||
exclude_params = [
|
||||
'exclude_transportations',
|
||||
'exclude_notes',
|
||||
'exclude_checklists',
|
||||
'exclude_lodging'
|
||||
]
|
||||
|
||||
for param in exclude_params:
|
||||
if self.request.query_params.get(param, 'false').lower() == 'true':
|
||||
context[param] = True
|
||||
|
||||
return context
|
||||
|
||||
def get_optimized_queryset_for_listing(self):
|
||||
"""Get optimized queryset for list actions with prefetching"""
|
||||
return self.get_base_queryset().select_related('user').prefetch_related(
|
||||
Prefetch(
|
||||
'locations__images',
|
||||
queryset=ContentImage.objects.filter(is_primary=True).select_related('user'),
|
||||
to_attr='primary_images'
|
||||
)
|
||||
)
|
||||
|
||||
def get_base_queryset(self):
|
||||
"""Base queryset logic extracted for reuse"""
|
||||
if self.action == 'destroy':
|
||||
return Collection.objects.filter(user=self.request.user.id)
|
||||
|
||||
if self.action in ['update', 'partial_update']:
|
||||
return Collection.objects.filter(
|
||||
Q(user=self.request.user.id) | Q(shared_with=self.request.user)
|
||||
).distinct()
|
||||
|
||||
# Allow access to collections with pending invites for accept/decline actions
|
||||
if self.action in ['accept_invite', 'decline_invite']:
|
||||
if not self.request.user.is_authenticated:
|
||||
return Collection.objects.none()
|
||||
return Collection.objects.filter(
|
||||
Q(user=self.request.user.id) |
|
||||
Q(shared_with=self.request.user) |
|
||||
Q(invites__invited_user=self.request.user)
|
||||
).distinct()
|
||||
|
||||
if self.action == 'retrieve':
|
||||
if not self.request.user.is_authenticated:
|
||||
return Collection.objects.filter(is_public=True)
|
||||
return Collection.objects.filter(
|
||||
Q(is_public=True) | Q(user=self.request.user.id) | Q(shared_with=self.request.user)
|
||||
).distinct()
|
||||
|
||||
# For list action, include collections owned by the user or shared with the user, that are not archived
|
||||
return Collection.objects.filter(
|
||||
(Q(user=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False)
|
||||
).distinct()
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get queryset with optimizations for list actions"""
|
||||
if self.action in ['list', 'all', 'archived', 'shared']:
|
||||
return self.get_optimized_queryset_for_listing()
|
||||
return self.get_base_queryset()
|
||||
|
||||
def list(self, request):
|
||||
# make sure the user is authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=400)
|
||||
queryset = Collection.objects.filter(user=request.user, is_archived=False)
|
||||
|
||||
queryset = Collection.objects.filter(
|
||||
(Q(user=request.user.id) | Q(shared_with=request.user)) & Q(is_archived=False)
|
||||
).distinct().select_related('user').prefetch_related(
|
||||
Prefetch(
|
||||
'locations__images',
|
||||
queryset=ContentImage.objects.filter(is_primary=True).select_related('user'),
|
||||
to_attr='primary_images'
|
||||
)
|
||||
)
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
return self.paginate_and_respond(queryset, request)
|
||||
|
||||
@@ -85,6 +151,12 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
queryset = Collection.objects.filter(
|
||||
Q(user=request.user)
|
||||
).select_related('user').prefetch_related(
|
||||
Prefetch(
|
||||
'locations__images',
|
||||
queryset=ContentImage.objects.filter(is_primary=True).select_related('user'),
|
||||
to_attr='primary_images'
|
||||
)
|
||||
)
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
@@ -99,6 +171,12 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
queryset = Collection.objects.filter(
|
||||
Q(user=request.user.id) & Q(is_archived=True)
|
||||
).select_related('user').prefetch_related(
|
||||
Prefetch(
|
||||
'locations__images',
|
||||
queryset=ContentImage.objects.filter(is_primary=True).select_related('user'),
|
||||
to_attr='primary_images'
|
||||
)
|
||||
)
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
@@ -173,9 +251,17 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
def shared(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=400)
|
||||
|
||||
queryset = Collection.objects.filter(
|
||||
shared_with=request.user
|
||||
).select_related('user').prefetch_related(
|
||||
Prefetch(
|
||||
'locations__images',
|
||||
queryset=ContentImage.objects.filter(is_primary=True).select_related('user'),
|
||||
to_attr='primary_images'
|
||||
)
|
||||
)
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
@@ -222,8 +308,6 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
# Add these methods to your CollectionViewSet class
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='revoke-invite/(?P<uuid>[^/.]+)')
|
||||
def revoke_invite(self, request, pk=None, uuid=None):
|
||||
"""Revoke a pending invite for a collection"""
|
||||
@@ -393,37 +477,6 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return Response({"success": success_message})
|
||||
|
||||
def get_queryset(self):
|
||||
if self.action == 'destroy':
|
||||
return Collection.objects.filter(user=self.request.user.id)
|
||||
|
||||
if self.action in ['update', 'partial_update']:
|
||||
return Collection.objects.filter(
|
||||
Q(user=self.request.user.id) | Q(shared_with=self.request.user)
|
||||
).distinct()
|
||||
|
||||
# Allow access to collections with pending invites for accept/decline actions
|
||||
if self.action in ['accept_invite', 'decline_invite']:
|
||||
if not self.request.user.is_authenticated:
|
||||
return Collection.objects.none()
|
||||
return Collection.objects.filter(
|
||||
Q(user=self.request.user.id) |
|
||||
Q(shared_with=self.request.user) |
|
||||
Q(invites__invited_user=self.request.user)
|
||||
).distinct()
|
||||
|
||||
if self.action == 'retrieve':
|
||||
if not self.request.user.is_authenticated:
|
||||
return Collection.objects.filter(is_public=True)
|
||||
return Collection.objects.filter(
|
||||
Q(is_public=True) | Q(user=self.request.user.id) | Q(shared_with=self.request.user)
|
||||
).distinct()
|
||||
|
||||
# For list action, include collections owned by the user or shared with the user, that are not archived
|
||||
return Collection.objects.filter(
|
||||
(Q(user=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False)
|
||||
).distinct()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# This is ok because you cannot share a collection when creating it
|
||||
serializer.save(user=self.request.user)
|
||||
@@ -435,13 +488,4 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
# Add nested=True to serializer context for GET list requests
|
||||
context = self.get_serializer_context()
|
||||
# If this is a list action, make sure nested=True in context
|
||||
if self.action == 'list':
|
||||
context['nested'] = True
|
||||
kwargs['context'] = context
|
||||
return super().get_serializer(*args, **kwargs)
|
||||
return Response(serializer.data)
|
||||
Reference in New Issue
Block a user