-
Notifications
You must be signed in to change notification settings - Fork 3
Contest community #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Contest community #740
Changes from all commits
668723e
b3a07fd
10fc9c0
4b5d766
f1d5bf3
6a46590
1264fe8
ff3a95b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Generated manually for contest community post visibility. | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("community", "0003_alter_post_question_status"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="post", | ||
| name="visibility", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("CONTEST_PARTICIPANTS", "대회 참여자 전체"), | ||
| ("CONTEST_HOSTS", "주최자만"), | ||
| ], | ||
| default="CONTEST_PARTICIPANTS", | ||
| max_length=30, | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ class ReplySerializer(serializers.ModelSerializer): | |
|
|
||
| author_name = serializers.CharField(source="author.username", read_only=True) | ||
| author_avatar = serializers.CharField(source="author.userprofile.avatar", read_only=True) | ||
| is_contest_host = serializers.SerializerMethodField() | ||
|
|
||
| class Meta: | ||
| model = Comment | ||
|
|
@@ -19,19 +20,25 @@ class Meta: | |
| "author", | ||
| "author_name", | ||
| "author_avatar", | ||
| "is_contest_host", | ||
| "content", | ||
| "parent_comment", | ||
| "created_at", | ||
| "updated_at", | ||
| ] | ||
|
|
||
| def get_is_contest_host(self, obj): | ||
| contest = self.context.get("contest") | ||
| return bool(contest and obj.author.is_contest_admin(contest)) | ||
|
|
||
|
|
||
| class CommentSerializer(serializers.ModelSerializer): | ||
| """댓글과 대댓글을 포함하는 Serializer""" | ||
|
|
||
| author_name = serializers.CharField(source="author.username", read_only=True) | ||
| author_avatar = serializers.CharField(source="author.userprofile.avatar", read_only=True) | ||
| replies = ReplySerializer(many=True, read_only=True) | ||
| is_contest_host = serializers.SerializerMethodField() | ||
|
|
||
| class Meta: | ||
| model = Comment | ||
|
|
@@ -41,6 +48,7 @@ class Meta: | |
| "author", | ||
| "author_name", | ||
| "author_avatar", | ||
| "is_contest_host", | ||
| "content", | ||
| "parent_comment", | ||
| "created_at", | ||
|
|
@@ -49,6 +57,10 @@ class Meta: | |
| ] | ||
| read_only_fields = ["post", "author"] | ||
|
|
||
| def get_is_contest_host(self, obj): | ||
| contest = self.context.get("contest") | ||
| return bool(contest and obj.author.is_contest_admin(contest)) | ||
|
|
||
|
|
||
| class PostListSerializer(serializers.ModelSerializer): | ||
| """게시글 목록을 위한 Serializer""" | ||
|
|
@@ -58,6 +70,8 @@ class PostListSerializer(serializers.ModelSerializer): | |
| author_avatar = serializers.CharField(source="author.userprofile.avatar", read_only=True) | ||
| community_type = serializers.SerializerMethodField() | ||
| comment_count = serializers.IntegerField(read_only=True) | ||
| is_mine = serializers.BooleanField(read_only=True) | ||
| can_view = serializers.BooleanField(read_only=True) | ||
|
|
||
| class Meta: | ||
| model = Post | ||
|
|
@@ -71,11 +85,14 @@ class Meta: | |
| "community_type", | ||
| "post_type", | ||
| "question_status", | ||
| "visibility", | ||
| "created_at", | ||
| "updated_at", | ||
| "problem", | ||
| "contest", | ||
| "comment_count", | ||
| "is_mine", | ||
| "can_view", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Adding Useful? React with 👍 / 👎. |
||
| ] | ||
|
|
||
| def get_content_preview(self, obj): | ||
|
|
@@ -112,6 +129,7 @@ class Meta: | |
| "community_type", | ||
| "post_type", | ||
| "question_status", | ||
| "visibility", | ||
| "created_at", | ||
| "updated_at", | ||
| "problem", | ||
|
|
@@ -130,7 +148,7 @@ def get_comments(self, obj): | |
| """게시글에 달린 댓글 중 최상위 댓글(대댓글이 아닌)만 필터링하여 반환합니다.""" | ||
| # view에서 prefetch_related를 사용했기 때문에 추가 쿼리가 발생하지 않습니다. | ||
| top_level_comments = [comment for comment in obj.comments.all() if comment.parent_comment_id is None] | ||
| serializer = CommentSerializer(top_level_comments, many=True) | ||
| serializer = CommentSerializer(top_level_comments, many=True, context={"contest": obj.contest}) | ||
| return serializer.data | ||
|
|
||
|
|
||
|
|
@@ -142,6 +160,7 @@ class CreatePostSerializer(serializers.Serializer): | |
| problem_id = serializers.IntegerField(required=False, allow_null=True) | ||
| contest_id = serializers.IntegerField(required=False, allow_null=True) | ||
| post_type = serializers.ChoiceField(choices=Post.PostType.choices, default=Post.PostType.ARTICLE) | ||
| visibility = serializers.ChoiceField(choices=Post.Visibility.choices, required=False) | ||
|
|
||
|
|
||
| class PostUpdateSerializer(serializers.Serializer): | ||
|
|
@@ -151,3 +170,4 @@ class PostUpdateSerializer(serializers.Serializer): | |
| content = serializers.CharField(required=False) | ||
| post_type = serializers.ChoiceField(choices=Post.PostType.choices, required=False) | ||
| question_status = serializers.ChoiceField(choices=Post.QuestionStatus.choices, required=False) | ||
| visibility = serializers.ChoiceField(choices=Post.Visibility.choices, required=False) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| from django.db.models import Count, Q | ||
| from django.db.models import BooleanField, Case, Count, Q, Value, When | ||
| from account.decorators import check_contest_permission, login_required | ||
| from contest.models import Contest | ||
| from problem.models import Problem | ||
|
|
@@ -37,6 +37,7 @@ def post(self, request): | |
| "content": data["content"], | ||
| "author": request.user, | ||
| "post_type": data["post_type"], | ||
| "visibility": data.get("visibility", Post.Visibility.CONTEST_PARTICIPANTS), | ||
| } | ||
|
|
||
| # 질문 게시글인 경우 기본 상태를 'OPEN'으로 설정 | ||
|
|
@@ -53,6 +54,8 @@ def post(self, request): | |
| post_data["contest_id"] = contest_id | ||
| except Contest.DoesNotExist: | ||
| return self.error("Contest does not exist") | ||
| else: | ||
| post_data["visibility"] = Post.Visibility.CONTEST_PARTICIPANTS | ||
|
|
||
| # problem_id가 주어진 경우 문제 존재 여부 확인 | ||
| if problem_id: | ||
|
|
@@ -74,8 +77,27 @@ def get(self, request): | |
| keyword = request.GET.get("keyword", "").strip() | ||
| sort_type = request.GET.get("sort_type") | ||
|
|
||
| posts = Post.objects.select_related("author__userprofile").annotate( | ||
| comment_count=Count('comments')).all().order_by("-created_at") | ||
| is_mine_condition = Q(author_id=request.user.id) if request.user.is_authenticated else Q(pk__isnull=True) | ||
| can_view_condition = Q(visibility=Post.Visibility.CONTEST_PARTICIPANTS) | Q(contest__isnull=True) | ||
| if request.user.is_authenticated: | ||
| can_view_condition |= Q(author_id=request.user.id) | ||
| can_view_condition |= Q(contest__created_by_id=request.user.id) | ||
| can_view_annotation = Value(True, output_field=BooleanField()) if ( | ||
| request.user.is_authenticated and request.user.is_super_admin() | ||
| ) else Case( | ||
| When(can_view_condition, then=Value(True)), | ||
| default=Value(False), | ||
| output_field=BooleanField(), | ||
| ) | ||
| posts = Post.objects.select_related("author__userprofile", "contest__created_by").annotate( | ||
| comment_count=Count('comments'), | ||
| is_mine=Case( | ||
| When(is_mine_condition, then=Value(True)), | ||
| default=Value(False), | ||
| output_field=BooleanField(), | ||
| ), | ||
| can_view=can_view_annotation, | ||
| ).all().order_by("-created_at") | ||
| if contest_id: | ||
| try: | ||
| self.contest = Contest.objects.get(id=contest_id, visible=True) | ||
|
|
@@ -138,6 +160,10 @@ def get(self, request, post_id): | |
| error = self._check_contest_permission(request) | ||
| if error: | ||
| return self.error("No permission to access this contest's community") | ||
| if (post.visibility == Post.Visibility.CONTEST_HOSTS and | ||
| post.author != request.user and | ||
| not request.user.is_contest_admin(post.contest)): | ||
|
Comment on lines
+163
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only blocks the post detail response for Useful? React with 👍 / 👎. |
||
| return self.error("Only contest hosts or the author can view this post") | ||
|
|
||
| return self.success(PostDetailSerializer(post).data) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a view preloads
self.contest, this branch skips the only lookup that enforcesvisible=True.PostDetailAPIViewnow setsself.contest = post.contest, so posts belonging to a contest later hidden/deleted in admin can still be read by any user who otherwise passes the public/password/status checks; please either validateself.contest.visiblehere or reload by id withvisible=True.Useful? React with 👍 / 👎.