diff --git a/backend/apps/quib/api/v1/serializers.py b/backend/apps/quib/api/v1/serializers.py index 6def86e..a7f5d4e 100644 --- a/backend/apps/quib/api/v1/serializers.py +++ b/backend/apps/quib/api/v1/serializers.py @@ -25,3 +25,9 @@ def get_cover(self, obj): if obj.cover: return request.build_absolute_uri(obj.cover) if request else obj.cover return None + + +class QuibHighlightedSerializer(serializers.ModelSerializer): + class Meta: + model = Quib + fields = ('cover', 'title', 'id', 'slug') diff --git a/backend/apps/quib/migrations/0004_quib_highlighted.py b/backend/apps/quib/migrations/0004_quib_highlighted.py new file mode 100644 index 0000000..ef20ca5 --- /dev/null +++ b/backend/apps/quib/migrations/0004_quib_highlighted.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2024-12-18 05:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('quib', '0003_rename_quibmodel_quib'), + ] + + operations = [ + migrations.AddField( + model_name='quib', + name='highlighted', + field=models.BooleanField(default=False, verbose_name='highlighted'), + ), + ] diff --git a/backend/apps/quib/models.py b/backend/apps/quib/models.py index 91fe914..a9fe474 100644 --- a/backend/apps/quib/models.py +++ b/backend/apps/quib/models.py @@ -26,6 +26,7 @@ class Quib(CreatedAtMixin, IsPublicMixin, ShortUUIDMixin): verbose_name=_('quibber'), on_delete=models.CASCADE, ) + highlighted = models.BooleanField(_('highlighted'), default=False) title = models.CharField(_('title'), max_length=255) slug = models.SlugField(_('slug'), editable=False, max_length=25, blank=True) content = models.TextField(_('content'), blank=True) diff --git a/backend/apps/quiblet/api/v1/viewsets.py b/backend/apps/quiblet/api/v1/viewsets.py index f57b985..00adb5d 100644 --- a/backend/apps/quiblet/api/v1/viewsets.py +++ b/backend/apps/quiblet/api/v1/viewsets.py @@ -6,7 +6,7 @@ from rest_framework import exceptions, response, viewsets from rest_framework.decorators import action -from apps.quib.api.v1.serializers import QuibSlimSerializer +from apps.quib.api.v1.serializers import QuibHighlightedSerializer, QuibSlimSerializer from common.patches.request import PatchedHttpRequest from ...models import Quiblet @@ -25,9 +25,11 @@ class QuibletViewSet(viewsets.ModelViewSet): # extra custom serializers serializer_classes = { + 'retrieve': QuibletDetailSerializer, + # extra actions 'exists': QuibletExistsSerializer, 'quibs': QuibSlimSerializer, - 'retrieve': QuibletDetailSerializer, + 'highlighted_quibs': QuibHighlightedSerializer, } def get_queryset(self) -> QuerySet[Quiblet]: # pyright: ignore @@ -68,6 +70,16 @@ def quibs(self, request, name=None): return response.Response(serializer.data) + @extend_schema(responses=QuibHighlightedSerializer(many=True)) + @action(detail=True, methods=[HTTPMethod.GET]) + def highlighted_quibs(self, request, name=None): + quibs = self.get_object().quibs.filter(highlighted=True) # pyright: ignore + serializer = QuibHighlightedSerializer( + 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 6b7a0f8..dae7604 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}/highlighted_quibs/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations['quiblets_highlighted_quibs_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/quiblets/{name}/quibs/': { parameters: { query?: never; @@ -689,6 +705,7 @@ export interface components { */ readonly created_at?: string; is_public?: boolean; + highlighted?: boolean; title?: string; readonly slug?: string; content?: string; @@ -742,6 +759,7 @@ export interface components { */ readonly created_at: string; is_public?: boolean; + highlighted?: boolean; title: string; readonly slug: string; content?: string; @@ -752,6 +770,13 @@ export interface components { downvotes?: number[]; comments?: number[]; }; + QuibHighlighted: { + /** Format: uri */ + cover?: string | null; + title: string; + readonly id: string; + readonly slug: string; + }; QuibSlim: { readonly id: string; readonly quiblet: components['schemas']['QuibletSlim']; @@ -761,6 +786,7 @@ export interface components { */ readonly created_at: string; is_public?: boolean; + highlighted?: boolean; title: string; readonly slug: string; content?: string; @@ -1560,6 +1586,7 @@ export interface components { QuibsCreateError: | components['schemas']['QuibsCreateNonFieldErrorsErrorComponent'] | components['schemas']['QuibsCreateIsPublicErrorComponent'] + | components['schemas']['QuibsCreateHighlightedErrorComponent'] | components['schemas']['QuibsCreateTitleErrorComponent'] | components['schemas']['QuibsCreateContentErrorComponent'] | components['schemas']['QuibsCreateCoverErrorComponent'] @@ -1567,6 +1594,20 @@ export interface components { | components['schemas']['QuibsCreateUpvotesErrorComponent'] | components['schemas']['QuibsCreateDownvotesErrorComponent'] | components['schemas']['QuibsCreateCommentsErrorComponent']; + QuibsCreateHighlightedErrorComponent: { + /** + * @description * `highlighted` - highlighted (enum property replaced by openapi-typescript) + * @enum {string} + */ + attr: 'highlighted'; + /** + * @description * `invalid` - invalid + * * `null` - null + * @enum {string} + */ + code: 'invalid' | 'null'; + detail: string; + }; QuibsCreateIsPublicErrorComponent: { /** * @description * `is_public` - is_public (enum property replaced by openapi-typescript) @@ -1729,6 +1770,7 @@ export interface components { QuibsPartialUpdateError: | components['schemas']['QuibsPartialUpdateNonFieldErrorsErrorComponent'] | components['schemas']['QuibsPartialUpdateIsPublicErrorComponent'] + | components['schemas']['QuibsPartialUpdateHighlightedErrorComponent'] | components['schemas']['QuibsPartialUpdateTitleErrorComponent'] | components['schemas']['QuibsPartialUpdateContentErrorComponent'] | components['schemas']['QuibsPartialUpdateCoverErrorComponent'] @@ -1736,6 +1778,20 @@ export interface components { | components['schemas']['QuibsPartialUpdateUpvotesErrorComponent'] | components['schemas']['QuibsPartialUpdateDownvotesErrorComponent'] | components['schemas']['QuibsPartialUpdateCommentsErrorComponent']; + QuibsPartialUpdateHighlightedErrorComponent: { + /** + * @description * `highlighted` - highlighted (enum property replaced by openapi-typescript) + * @enum {string} + */ + attr: 'highlighted'; + /** + * @description * `invalid` - invalid + * * `null` - null + * @enum {string} + */ + code: 'invalid' | 'null'; + detail: string; + }; QuibsPartialUpdateIsPublicErrorComponent: { /** * @description * `is_public` - is_public (enum property replaced by openapi-typescript) @@ -1898,6 +1954,7 @@ export interface components { QuibsUpdateError: | components['schemas']['QuibsUpdateNonFieldErrorsErrorComponent'] | components['schemas']['QuibsUpdateIsPublicErrorComponent'] + | components['schemas']['QuibsUpdateHighlightedErrorComponent'] | components['schemas']['QuibsUpdateTitleErrorComponent'] | components['schemas']['QuibsUpdateContentErrorComponent'] | components['schemas']['QuibsUpdateCoverErrorComponent'] @@ -1905,6 +1962,20 @@ export interface components { | components['schemas']['QuibsUpdateUpvotesErrorComponent'] | components['schemas']['QuibsUpdateDownvotesErrorComponent'] | components['schemas']['QuibsUpdateCommentsErrorComponent']; + QuibsUpdateHighlightedErrorComponent: { + /** + * @description * `highlighted` - highlighted (enum property replaced by openapi-typescript) + * @enum {string} + */ + attr: 'highlighted'; + /** + * @description * `invalid` - invalid + * * `null` - null + * @enum {string} + */ + code: 'invalid' | 'null'; + detail: string; + }; QuibsUpdateIsPublicErrorComponent: { /** * @description * `is_public` - is_public (enum property replaced by openapi-typescript) @@ -3018,6 +3089,43 @@ export interface operations { }; }; }; + quiblets_highlighted_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']['QuibHighlighted'][]; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse404']; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse500']; + }; + }; + }; + }; quiblets_quibs_list: { parameters: { query?: never; diff --git a/frontend/src/routes/(app)/q/[name]/+page.server.ts b/frontend/src/routes/(app)/q/[name]/+page.server.ts index b4fdce6..cf9b4e2 100644 --- a/frontend/src/routes/(app)/q/[name]/+page.server.ts +++ b/frontend/src/routes/(app)/q/[name]/+page.server.ts @@ -2,15 +2,34 @@ 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/', { + const { + data: quibs_data, + error: quibs_error, + response: quibs_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); + const { + data: highlighted_quibs_data, + error: highlighted_quibs_error, + response: highlighted_quibs_response + } = await client.GET('/api/v1/quiblets/{name}/highlighted_quibs/', { + params: { + path: { name: params.name } + } + }); + + if ( + quibs_response.ok && + quibs_data && + highlighted_quibs_response && + highlighted_quibs_data + ) { + return { quibs: quibs_data, highlighted_quibs: highlighted_quibs_data }; + } else if (quibs_error || highlighted_quibs_error) { + console.error(quibs_error, highlighted_quibs_error); } }; diff --git a/frontend/src/routes/(app)/q/[name]/+page.svelte b/frontend/src/routes/(app)/q/[name]/+page.svelte index 744fd9c..78832f9 100644 --- a/frontend/src/routes/(app)/q/[name]/+page.svelte +++ b/frontend/src/routes/(app)/q/[name]/+page.svelte @@ -6,7 +6,7 @@ import type { PageData } from './$types'; const { data }: { data: PageData } = $props(); - const { quiblet, quibs } = data; + const { quiblet, quibs, highlighted_quibs } = data; @@ -42,6 +42,35 @@
+{#if highlighted_quibs} +
+
+ +

Highlights

+
+
+ {#each highlighted_quibs as quib} +
+
+ {quib.title} + +
+ {/each} +
+
+{/if} {#if quibs} {#each quibs as quib}