Use hub 'name' in routes & add top-post sorting

Switch hub endpoints to use the hub `name` slug and update frontend routes/clients accordingly. Backend: HubViewSet now uses lookup_field='name'; PostViewSet list supports `sort=top` with vote_score annotation and time windows/custom ranges, and a new TopPostsCursorPagination was added. Frontend: routes changed from `/hub/:id` to `/h/:name`, the generated hubs API was updated from id->name, and the hub feed client accepts `sort`, `time`, `start`, and `end` params (query key updated). Also adds new homepage UI components (HeroSection, DroneSection) and navbar improvements (scroll state, auto-close mobile menu on route changes, and small icon/class tweaks).
This commit is contained in:
2026-06-07 12:19:40 +02:00
parent cb23abeb5f
commit ad1f6a90b6
29 changed files with 1778 additions and 559 deletions

View File

@@ -58,6 +58,7 @@ from .serializers import HubPermissionSerializer, HubSerializer, TagsSerializer,
class HubViewSet(viewsets.ModelViewSet):
serializer_class = HubSerializer
permission_classes = [CanEditHub]
lookup_field = 'name'
filterset_fields = ['is_public', 'owner']
search_fields = ['name', 'description']
ordering_fields = ['name']

View File

@@ -1,4 +1,8 @@
from django.db.models import Count, Q
from datetime import timedelta
from django.db.models import Count, Q, Sum
from django.db.models.functions import Coalesce
from django.utils import timezone
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
@@ -8,7 +12,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiPara
from rest_framework.permissions import IsAuthenticated
from social.hubs.models import Tags
from vontor_cz.pagination import CreatedCursorPagination
from vontor_cz.pagination import CreatedCursorPagination, TopPostsCursorPagination
from .models import Post, PostContent, PostVote, PostSave
from .permissions import CanDeletePost, IsPostAuthorOnly
from .serializers import PostSerializer, PostContentSerializer, PostVoteSerializer, TagAttachSerializer
@@ -78,6 +82,51 @@ class PostViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(author=self.request.user)
_TIME_WINDOWS = {
'1h': timedelta(hours=1),
'6h': timedelta(hours=6),
'day': timedelta(days=1),
'week': timedelta(weeks=1),
'month': timedelta(days=30),
'year': timedelta(days=365),
}
def _get_cutoff(self, time_param):
"""Return a datetime cutoff for the given time window, or None for 'all'."""
if time_param in self._TIME_WINDOWS:
return timezone.now() - self._TIME_WINDOWS[time_param]
return None
def list(self, request, *args, **kwargs):
sort = request.query_params.get('sort', 'newest')
time_param = request.query_params.get('time', 'all')
qs = self.filter_queryset(self.get_queryset())
# Time filter
if time_param == 'custom':
start = request.query_params.get('start')
end = request.query_params.get('end')
if start:
qs = qs.filter(created_at__date__gte=start)
if end:
qs = qs.filter(created_at__date__lte=end)
else:
cutoff = self._get_cutoff(time_param)
if cutoff:
qs = qs.filter(created_at__gte=cutoff)
if sort == 'top':
qs = qs.annotate(vote_score=Coalesce(Sum('votes__vote'), 0)).order_by('-vote_score', '-id')
paginator = TopPostsCursorPagination()
else:
qs = qs.order_by('-created_at')
paginator = CreatedCursorPagination()
page = paginator.paginate_queryset(qs, request, view=self)
ser = PostSerializer(page, many=True, context={'request': request})
return paginator.get_paginated_response(ser.data)
# ------------------------------------------------------------------
# Media upload action
# ------------------------------------------------------------------