posts are done

This commit is contained in:
2026-05-19 00:08:02 +02:00
parent 202ce22102
commit 2e9e3ed41b
35 changed files with 1528 additions and 272 deletions

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-05-18 19:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('posts', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='PostSave',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saves', to='posts.post')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saved_posts', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('post', 'user')},
},
),
]

View File

@@ -82,6 +82,18 @@ class PostContent(SoftDeleteModel):
return super().save(*args, **kwargs)
class PostSave(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='saves')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='saved_posts')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('post', 'user')
def __str__(self):
return f"{self.user} saved Post {self.post_id}"
class PostVote(SoftDeleteModel):
class VoteChoice(models.IntegerChoices):
UP = 1, 'Upvote'

View File

@@ -1,6 +1,6 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import Post, PostContent, PostVote
from .models import Post, PostContent, PostVote, PostSave
from social.hubs.serializers import TagsSerializer
User = get_user_model()
@@ -28,6 +28,8 @@ class PostSerializer(serializers.ModelSerializer):
vote_score = serializers.SerializerMethodField()
user_vote = serializers.SerializerMethodField()
reply_count = serializers.IntegerField(read_only=True, default=0)
is_saved = serializers.SerializerMethodField()
save_count = serializers.SerializerMethodField()
class Meta:
model = Post
@@ -36,7 +38,7 @@ class PostSerializer(serializers.ModelSerializer):
'author', 'author_detail',
'hub', 'reply_to',
'tags', 'contents',
'vote_score', 'user_vote', 'reply_count',
'vote_score', 'user_vote', 'reply_count', 'is_saved', 'save_count',
]
read_only_fields = ['author', 'created_at', 'updated_at']
@@ -50,6 +52,15 @@ class PostSerializer(serializers.ModelSerializer):
vote = obj.votes.filter(user=request.user).first()
return vote.vote if vote else 0
def get_is_saved(self, obj):
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
return obj.saves.filter(user=request.user).exists()
def get_save_count(self, obj):
return obj.saves.count()
class PostVoteSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -9,7 +9,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 .models import Post, PostContent, PostVote
from .models import Post, PostContent, PostVote, PostSave
from .permissions import CanDeletePost, IsPostAuthorOnly
from .serializers import PostSerializer, PostContentSerializer, PostVoteSerializer, TagAttachSerializer
@@ -67,7 +67,7 @@ class PostViewSet(viewsets.ModelViewSet):
qs = (
Post.objects
.select_related('author', 'hub')
.prefetch_related('tags', 'contents', 'votes')
.prefetch_related('tags', 'contents', 'votes', 'saves')
.annotate(reply_count=Count('replies', distinct=True))
)
hub_id = self.request.query_params.get('hub')
@@ -196,7 +196,7 @@ class PostViewSet(viewsets.ModelViewSet):
base_qs = (
Post.objects
.select_related('author', 'hub')
.prefetch_related('tags', 'contents', 'votes')
.prefetch_related('tags', 'contents', 'votes', 'saves')
.annotate(reply_count=Count('replies', distinct=True))
.filter(reply_to__isnull=True)
)
@@ -241,3 +241,45 @@ class PostViewSet(viewsets.ModelViewSet):
defaults={'vote': ser.validated_data['vote']},
)
return Response(PostVoteSerializer(vote_obj).data)
# ------------------------------------------------------------------
# Save / unsave action
# ------------------------------------------------------------------
@extend_schema(
tags=["posts"],
summary="Toggle save on a post",
description="Saves the post for the current user, or unsaves it if already saved. Returns `{saved: true/false}`.",
responses={200: {'type': 'object', 'properties': {'saved': {'type': 'boolean'}}}},
)
@action(detail=True, methods=['post'], url_path='save')
def save_post(self, request, pk=None):
post = self.get_object()
obj, created = PostSave.objects.get_or_create(post=post, user=request.user)
if not created:
obj.delete()
return Response({'saved': created})
# ------------------------------------------------------------------
# Saved feed
# ------------------------------------------------------------------
@extend_schema(
tags=["posts"],
summary="List posts saved by the current user",
responses={200: PostSerializer(many=True)},
)
@action(detail=False, methods=['get'], url_path='saved')
def saved(self, request):
qs = (
Post.objects
.select_related('author', 'hub')
.prefetch_related('tags', 'contents', 'votes', 'saves')
.annotate(reply_count=Count('replies', distinct=True))
.filter(saves__user=request.user)
.order_by('-saves__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)