feat: add community contribution ranks

This commit is contained in:
2026-09-29 22:18:57 +03:30
parent 0f6220f72c
commit 77567178ad
46 changed files with 2206 additions and 23 deletions

View File

@@ -100,23 +100,41 @@ class PostAdmin(BaseModelAdmin, ImportExportModelAdmin):
actions = BaseModelAdmin.actions + ['submit_for_review', 'request_changes', 'make_published', 'make_draft', 'make_featured', 'restore_posts']
def submit_for_review(self, request, queryset):
queryset.update(status='submitted')
self.message_user(request, f"Submitted {queryset.count()} posts for review.")
count = 0
for post in queryset:
post.status = Post.StatusChoices.SUBMITTED
post.save(update_fields=["status", "updated_at"])
count += 1
self.message_user(request, f"Submitted {count} posts for review.")
submit_for_review.short_description = "Submit selected posts for review"
def request_changes(self, request, queryset):
queryset.update(status='changes_requested', reviewed_by=request.user)
self.message_user(request, f"Requested changes on {queryset.count()} posts.")
count = 0
for post in queryset:
post.status = Post.StatusChoices.CHANGES_REQUESTED
post.reviewed_by = request.user
post.save(update_fields=["status", "reviewed_by", "updated_at"])
count += 1
self.message_user(request, f"Requested changes on {count} posts.")
request_changes.short_description = "Request changes on selected posts"
def make_published(self, request, queryset):
queryset.update(status='published', published_by=request.user)
self.message_user(request, f"Published {queryset.count()} posts.")
count = 0
for post in queryset:
post.status = Post.StatusChoices.PUBLISHED
post.published_by = request.user
post.save(update_fields=["status", "published_by", "published_at", "updated_at"])
count += 1
self.message_user(request, f"Published {count} posts.")
make_published.short_description = "Mark selected posts as published"
def make_draft(self, request, queryset):
queryset.update(status='draft')
self.message_user(request, f"Marked {queryset.count()} posts as draft.")
count = 0
for post in queryset:
post.status = Post.StatusChoices.DRAFT
post.save(update_fields=["status", "updated_at"])
count += 1
self.message_user(request, f"Marked {count} posts as draft.")
make_draft.short_description = "Mark selected posts as draft"
def make_featured(self, request, queryset):
@@ -156,8 +174,19 @@ class CommentAdmin(BaseModelAdmin):
content_preview.short_description = 'Content Preview'
def approve_comments(self, request, queryset):
queryset.update(is_approved=True, is_hidden=False, hidden_by=None, hidden_at=None, moderation_note='')
self.message_user(request, f"Approved {queryset.count()} comments.")
count = 0
for comment in queryset:
comment.is_approved = True
comment.is_hidden = False
comment.hidden_by = None
comment.hidden_at = None
comment.moderation_note = ""
comment.save(update_fields=[
"is_approved", "is_hidden", "hidden_by", "hidden_at",
"moderation_note", "updated_at",
])
count += 1
self.message_user(request, f"Approved {count} comments.")
approve_comments.short_description = "Approve selected comments"
def hide_comments(self, request, queryset):

View File

@@ -356,6 +356,22 @@ class BlogProfileActivitySchema(Schema):
replies: List[CommentSchema]
class PaginatedProfilePostsSchema(Schema):
count: int
page: int
page_size: int
total_pages: int
results: List[PostListSchema]
class PaginatedProfileCommentsSchema(Schema):
count: int
page: int
page_size: int
total_pages: int
results: List[CommentSchema]
class BlogBannerSchema(ModelSchema):
image_url: str

View File

@@ -20,6 +20,8 @@ from apps.blog.api.schemas import (
BlogFiltersSchema,
BlogInteractionSchema,
BlogProfileActivitySchema,
PaginatedProfileCommentsSchema,
PaginatedProfilePostsSchema,
CategorySchema,
CategoryWriteSchema,
CommentCreateSchema,
@@ -608,6 +610,48 @@ def my_blog_activity(request):
}
def _profile_page_payload(queryset, page: int, page_size: int = 10):
count = queryset.count()
total_pages = max(1, (count + page_size - 1) // page_size)
start = (page - 1) * page_size
return {
"count": count,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
"results": list(queryset[start : start + page_size]),
}
@blog_router.get("/me/activity/liked", response=PaginatedProfilePostsSchema, auth=jwt_auth)
def my_liked_posts(request, page: int = Query(1, ge=1)):
queryset = _published_queryset().filter(likes__user=request.auth).order_by("-published_at", "-created_at", "-id")
return _profile_page_payload(queryset, page)
@blog_router.get("/me/activity/saved", response=PaginatedProfilePostsSchema, auth=jwt_auth)
def my_saved_posts(request, page: int = Query(1, ge=1)):
queryset = _published_queryset().filter(saves__user=request.auth).order_by("-published_at", "-created_at", "-id")
return _profile_page_payload(queryset, page)
@blog_router.get("/me/activity/comments", response=PaginatedProfileCommentsSchema, auth=jwt_auth)
def my_blog_comments(request, page: int = Query(1, ge=1)):
queryset = (
Comment.objects.filter(
author=request.auth,
is_approved=True,
is_hidden=False,
is_deleted=False,
post__status=Post.StatusChoices.PUBLISHED,
post__is_deleted=False,
)
.select_related("author", "post")
.order_by("-created_at", "-id")
)
return _profile_page_payload(queryset, page)
@blog_router.get("/banners", response=List[BlogBannerSchema])
def list_banners(request):
return BlogBanner.objects.filter(is_active=True, is_deleted=False).order_by("sort_order", "-created_at")

View File

@@ -374,6 +374,9 @@ class Comment(BaseModel):
moderation_note=note,
updated_at=now,
)
from apps.gamification.services import sync_comment
for comment in self.__class__.all_objects.filter(id__in=ids).select_related("author", "post"):
sync_comment(comment)
def unhide(self):
now = timezone.now()
@@ -386,6 +389,9 @@ class Comment(BaseModel):
moderation_note="",
updated_at=now,
)
from apps.gamification.services import sync_comment
for comment in self.__class__.all_objects.filter(id__in=ids).select_related("author", "post"):
sync_comment(comment)
def descendant_ids(self) -> list[int]:
pending = [self.id]
@@ -408,6 +414,9 @@ class Comment(BaseModel):
delete_note=note,
updated_at=now,
)
from apps.gamification.services import sync_comment
for comment in self.__class__.all_objects.filter(id__in=ids).select_related("author", "post"):
sync_comment(comment)
class Like(models.Model):

View File

@@ -89,6 +89,33 @@ def test_blog_interactions_and_activity(client, blog_content, user_factory, auth
assert not Like.objects.filter(post=post, user=member).exists()
def test_my_blog_activity_paginates_each_category_independently(client, blog_content, user_factory, auth_headers):
_, _, _, _, _, _, _ = blog_content
member = user_factory()
headers = auth_headers(member)
posts = [
Post.objects.create(title=f"Saved and liked {index}", content="Content", author=member,
status=Post.StatusChoices.PUBLISHED)
for index in range(12)
]
for post in posts:
Like.objects.create(post=post, user=member)
SavedPost.objects.create(post=post, user=member)
Comment.objects.create(post=post, author=member, content=f"Comment {post.id}")
liked_page = client.get("/api/blog/me/activity/liked", {"page": 1}, **headers)
saved_page = client.get("/api/blog/me/activity/saved", {"page": 2}, **headers)
comments_page = client.get("/api/blog/me/activity/comments", {"page": 2}, **headers)
assert liked_page.status_code == 200, liked_page.content
assert liked_page.json()["count"] == 12
assert liked_page.json()["page_size"] == 10
assert len(liked_page.json()["results"]) == 10
assert len(saved_page.json()["results"]) == 2
assert len(comments_page.json()["results"]) == 2
assert comments_page.json()["total_pages"] == 2
def test_blog_editor_review_and_moderation(client, blog_content, user_factory, auth_headers, mocker):
author, _, _, _, _, _, _ = blog_content
editor = user_factory(is_staff=True)