diff --git a/backend/apps/quib/api/v1/serializers.py b/backend/apps/quib/api/v1/serializers.py index 87264a41..6def86ee 100644 --- a/backend/apps/quib/api/v1/serializers.py +++ b/backend/apps/quib/api/v1/serializers.py @@ -19,3 +19,9 @@ class QuibSlimSerializer(serializers.ModelSerializer): class Meta: model = Quib exclude = ('quibber',) + + def get_cover(self, obj): + request = self.context.get('request') + if obj.cover: + return request.build_absolute_uri(obj.cover) if request else obj.cover + return None diff --git a/backend/apps/quiblet/api/v1/serializers.py b/backend/apps/quiblet/api/v1/serializers.py index e94f326c..3f29cbc7 100644 --- a/backend/apps/quiblet/api/v1/serializers.py +++ b/backend/apps/quiblet/api/v1/serializers.py @@ -1,20 +1,36 @@ -from rest_framework import exceptions, serializers +from rest_framework import serializers + +from apps.user.models import Profile from ...models import Quiblet -class QuibletSerializer(serializers.ModelSerializer): +class RangerSerializer(serializers.ModelSerializer): + name = serializers.SerializerMethodField() + + class Meta: + model = Profile + fields = ('username', 'avatar', 'name') + + def get_name(self, obj): + if obj.first_name or obj.last_name: + truthy_fields = filter(None, [obj.first_name, obj.last_name]) + return " ".join(truthy_fields) + return None + + +class QuibletDetailSerializer(serializers.ModelSerializer): + rangers = RangerSerializer(many=True) + class Meta: model = Quiblet fields = '__all__' - def validate_name(self, name): - if Quiblet.objects.filter(name__iexact=name).exists(): - raise exceptions.ValidationError( - f"Quiblet with name {name} already exists (case-insensitive)." - ) - return name +class QuibletSerializer(serializers.ModelSerializer): + class Meta: + model = Quiblet + fields = '__all__' class QuibletSlimSerializer(serializers.ModelSerializer): diff --git a/backend/apps/quiblet/api/v1/viewsets.py b/backend/apps/quiblet/api/v1/viewsets.py index 58d54fbd..f57b985a 100644 --- a/backend/apps/quiblet/api/v1/viewsets.py +++ b/backend/apps/quiblet/api/v1/viewsets.py @@ -2,20 +2,34 @@ from typing import cast from django.db.models import QuerySet +from drf_spectacular.utils import extend_schema from rest_framework import exceptions, response, viewsets from rest_framework.decorators import action +from apps.quib.api.v1.serializers import QuibSlimSerializer from common.patches.request import PatchedHttpRequest from ...models import Quiblet -from .serializers import QuibletExistsSerializer, QuibletSerializer +from .serializers import ( + QuibletDetailSerializer, + QuibletExistsSerializer, + QuibletSerializer, +) class QuibletViewSet(viewsets.ModelViewSet): queryset = Quiblet.objects.all() + # default serializer serializer_class = QuibletSerializer lookup_field = 'name' + # extra custom serializers + serializer_classes = { + 'exists': QuibletExistsSerializer, + 'quibs': QuibSlimSerializer, + 'retrieve': QuibletDetailSerializer, + } + def get_queryset(self) -> QuerySet[Quiblet]: # pyright: ignore return super().get_queryset() @@ -31,8 +45,8 @@ def get_object(self) -> Quiblet: # pyright: ignore return obj def get_serializer_class(self): # pyright: ignore - if self.action == 'exists': - return QuibletExistsSerializer + if self.action in self.serializer_classes: + return self.serializer_classes[self.action] return super().get_serializer_class() @action(detail=True, methods=[HTTPMethod.GET]) @@ -46,6 +60,14 @@ def exists(self, request, name=None): return response.Response(res) + @extend_schema(responses=QuibSlimSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def quibs(self, request, name=None): + quibs = self.get_object().quibs.all() # pyright: ignore + serializer = QuibSlimSerializer(quibs, many=True, context={'request': request}) + + return response.Response(serializer.data) + def perform_create(self, serializer): patched_request = cast(PatchedHttpRequest, self.request) diff --git a/frontend/src/lib/clients/v1.ts b/frontend/src/lib/clients/v1.ts index 0ffdf9dd..041fe419 100644 --- a/frontend/src/lib/clients/v1.ts +++ b/frontend/src/lib/clients/v1.ts @@ -84,6 +84,22 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/quiblets/{name}/quibs/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['quiblets_quibs_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/quibs/': { parameters: { query?: never; @@ -772,6 +788,24 @@ export interface components { members?: number[]; rangers?: number[]; }; + QuibletDetail: { + readonly id: number; + rangers: components['schemas']['Ranger'][]; + /** + * Create at + * Format: date-time + */ + readonly created_at: string; + /** Format: uri */ + avatar?: string | null; + is_public?: boolean; + name: string; + description: string; + title?: string | null; + /** Format: uri */ + banner?: string | null; + members?: number[]; + }; QuibletExists: { exists: boolean; name: string; @@ -1960,6 +1994,13 @@ export interface components { type: components['schemas']['ValidationErrorEnum']; errors: components['schemas']['QuibsUpdateError'][]; }; + Ranger: { + /** @description Required. 25 characters or fewer. Letters, digits and ./_ only. */ + username: string; + /** Format: uri */ + avatar?: string | null; + readonly name: string; + }; /** * @description * `server_error` - Server Error * @enum {string} @@ -2780,7 +2821,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['Quiblet']; + 'application/json': components['schemas']['QuibletDetail']; }; }; 404: { @@ -2976,6 +3017,43 @@ export interface operations { }; }; }; + quiblets_quibs_list: { + parameters: { + query?: never; + header?: never; + path: { + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['QuibSlim'][]; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse404']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse500']; + }; + }; + }; + }; quibs_list: { parameters: { query?: never; diff --git a/frontend/src/routes/(app)/q/[name]/+layout.svelte b/frontend/src/routes/(app)/q/[name]/+layout.svelte index 27b5b9dd..ffe0c864 100644 --- a/frontend/src/routes/(app)/q/[name]/+layout.svelte +++ b/frontend/src/routes/(app)/q/[name]/+layout.svelte @@ -3,6 +3,7 @@ import type { PageData } from './$types'; import { FormatDate } from '$lib/functions/date'; import { pluralize } from '$lib/functions/pluralize'; + import Avatar from '$lib/components/ui/avatar.svelte'; const { data, children }: { data: PageData; children: Snippet } = $props(); const { quiblet } = data; @@ -35,5 +36,27 @@ > +
+
+

Rangers

+
+ +
+
+ {#if quiblet?.rangers} +
+ {#each quiblet?.rangers as ranger} +
+ +
+ u/{ranger.username} + {ranger.name} +
+
+ {/each} +
+ {/if} diff --git a/frontend/src/routes/(app)/q/[name]/+page.server.ts b/frontend/src/routes/(app)/q/[name]/+page.server.ts new file mode 100644 index 00000000..b4fdce67 --- /dev/null +++ b/frontend/src/routes/(app)/q/[name]/+page.server.ts @@ -0,0 +1,16 @@ +import client from '$lib/clients/client'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + const { data, error, response } = await client.GET('/api/v1/quiblets/{name}/quibs/', { + params: { + path: { name: params.name } + } + }); + + if (response.ok && data) { + return { quibs: data }; + } else if (error) { + console.error(error); + } +}; diff --git a/frontend/src/routes/(app)/q/[name]/+page.svelte b/frontend/src/routes/(app)/q/[name]/+page.svelte index 923294bd..744fd9c4 100644 --- a/frontend/src/routes/(app)/q/[name]/+page.svelte +++ b/frontend/src/routes/(app)/q/[name]/+page.svelte @@ -1,10 +1,12 @@ @@ -22,7 +24,7 @@
-

q/{quiblet?.name}

+

q/{quiblet?.name}

+
+ +{#if quibs} + {#each quibs as quib} + + {/each} +{/if}