Skip to content

Commit

Permalink
Merge pull request #385 from sparcs-kaist/feat/top-articles
Browse files Browse the repository at this point in the history
Add top article feature
  • Loading branch information
injoonH authored Jul 2, 2023
2 parents 432a239 + f53e72d commit 5d8937a
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 144 deletions.
2 changes: 1 addition & 1 deletion apps/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class ArticleAdmin(MetaDataModelAdmin):
"content_updated_at",
"commented_at",
"url",
"hidden_at",
("hidden_at", "topped_at"),
)
readonly_fields = (
"hit_count",
Expand Down
20 changes: 20 additions & 0 deletions apps/core/migrations/0049_article_topped_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.16 on 2023-05-11 11:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0048_update_board_name_type_flag"),
]

operations = [
migrations.AddField(
model_name="article",
name="topped_at",
field=models.DateTimeField(
blank=True, default=None, null=True, verbose_name="인기글 달성 시각"
),
),
]
18 changes: 18 additions & 0 deletions apps/core/migrations/0050_board_top_threshold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2023-05-11 13:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0049_article_topped_at"),
]

operations = [
migrations.AddField(
model_name="board",
name="top_threshold",
field=models.SmallIntegerField(default=10, verbose_name="인기글 달성 기준 좋아요 개수"),
),
]
91 changes: 47 additions & 44 deletions apps/core/models/article.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,128 +37,125 @@ class ArticleHiddenReason(str, Enum):


class Article(MetaDataModel):
class Meta(MetaDataModel.Meta):
verbose_name = "게시물"
verbose_name_plural = "게시물 목록"

title = models.CharField(
max_length=256,
verbose_name="제목",
max_length=256,
)
content = models.TextField(
verbose_name="본문",
)
content_text = models.TextField(
editable=False,
verbose_name="text 형식 본문",
editable=False,
)

name_type = models.SmallIntegerField(
default=NameType.REGULAR,
verbose_name="익명 혹은 실명 여부",
default=NameType.REGULAR,
)
is_content_sexual = models.BooleanField(
default=False,
verbose_name="성인/음란성 내용",
default=False,
)
is_content_social = models.BooleanField(
default=False,
verbose_name="정치/사회성 내용",
default=False,
)

hit_count = models.IntegerField(
default=0,
verbose_name="조회수",
default=0,
)
comment_count = models.IntegerField(
default=0,
verbose_name="댓글 수",
default=0,
)
report_count = models.IntegerField(
default=0,
verbose_name="신고 수",
default=0,
)
positive_vote_count = models.IntegerField(
default=0,
verbose_name="좋아요 수",
default=0,
)
negative_vote_count = models.IntegerField(
default=0,
verbose_name="싫어요 수",
default=0,
)

migrated_hit_count = models.IntegerField(
default=0,
verbose_name="이전된 조회수",
default=0,
)
migrated_positive_vote_count = models.IntegerField(
default=0,
verbose_name="이전된 좋아요 수",
default=0,
)
migrated_negative_vote_count = models.IntegerField(
default=0,
verbose_name="이전된 싫어요 수",
default=0,
)

created_by = models.ForeignKey(
on_delete=models.CASCADE,
verbose_name="작성자",
to=settings.AUTH_USER_MODEL,
db_index=True,
on_delete=models.CASCADE,
related_name="article_set",
verbose_name="작성자",
db_index=True,
)
parent_topic = models.ForeignKey(
on_delete=models.CASCADE,
verbose_name="말머리",
to="core.Topic",
null=True,
on_delete=models.CASCADE,
related_name="article_set",
blank=True,
default=None,
null=True,
db_index=True,
related_name="article_set",
verbose_name="말머리",
default=None,
)
parent_board = models.ForeignKey(
on_delete=models.CASCADE,
verbose_name="게시판",
to="core.Board",
db_index=True,
on_delete=models.CASCADE,
related_name="article_set",
verbose_name="게시판",
db_index=True,
)

attachments = models.ManyToManyField(
verbose_name="첨부 파일(들)",
to="core.Attachment",
blank=True,
db_index=True,
verbose_name="첨부 파일(들)",
)

commented_at = models.DateTimeField(
verbose_name="마지막 댓글 시간",
null=True,
default=None,
verbose_name="마지막 댓글 시간",
)

url = models.URLField(
null=True,
verbose_name="포탈 링크",
max_length=200,
blank=True,
null=True,
default=None,
verbose_name="포탈 링크",
)

content_updated_at = models.DateTimeField(
verbose_name="제목/본문/첨부파일 수정 시간",
null=True,
default=None,
verbose_name="제목/본문/첨부파일 수정 시간",
)

hidden_at = models.DateTimeField(
verbose_name="숨김 시간",
blank=True,
null=True,
default=None,
)
topped_at = models.DateTimeField(
verbose_name="인기글 달성 시각",
blank=True,
null=True,
default=None,
verbose_name="숨김 시간",
)

class Meta(MetaDataModel.Meta):
verbose_name = "게시물"
verbose_name_plural = "게시물 목록"

def __str__(self):
return self.title

Expand Down Expand Up @@ -223,7 +220,7 @@ def update_report_count(self):

self.save()

def update_vote_status(self):
def update_vote_status(self) -> None:
self.positive_vote_count = (
self.vote_set.filter(is_positive=True).count()
+ self.migrated_positive_vote_count
Expand All @@ -233,6 +230,12 @@ def update_vote_status(self):
+ self.migrated_negative_vote_count
)

if (
self.topped_at is None
and self.positive_vote_count >= self.parent_board.top_threshold
):
self.topped_at = timezone.now()

if (
self.parent_board.is_school_communication
and self.positive_vote_count >= SCHOOL_RESPONSE_VOTE_THRESHOLD
Expand Down
68 changes: 36 additions & 32 deletions apps/core/models/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,18 @@ class BoardAccessPermissionType(IntEnum):


class Board(MetaDataModel):
class Meta(MetaDataModel.Meta):
verbose_name = "게시판"
verbose_name_plural = "게시판 목록"
unique_together = (
("ko_name", "deleted_at"),
("en_name", "deleted_at"),
)

slug = AutoSlugField(
populate_from=[
"en_name",
],
)
ko_name = models.CharField(
max_length=32,
verbose_name="게시판 국문 이름",
max_length=32,
)
en_name = models.CharField(
max_length=32,
verbose_name="게시판 영문 이름",
max_length=32,
)
ko_description = models.TextField(
verbose_name="게시판 국문 소개",
Expand All @@ -51,70 +43,82 @@ class Meta(MetaDataModel.Meta):
# 사용자 그룹의 값들은 `UserGroup`을 참고하세요.
read_access_mask = models.SmallIntegerField(
# UNAUTHORIZED, EXTERNAL_ORG 제외 모든 사용자 읽기 권한 부여
default=0b011011110,
null=False,
verbose_name="읽기 권한",
default=0b011011110,
)
write_access_mask = models.SmallIntegerField(
# UNAUTHORIZED, STORE_EMPLOYEE, EXTERNAL_ORG 제외 모든 사용자 쓰기 권한 부여
default=0b011011010,
null=False,
verbose_name="쓰기 권한",
default=0b011011010,
)
comment_access_mask = models.SmallIntegerField(
# UNAUTHORIZED 제외 모든 사용자 댓글 권한 부여
default=0b011111110,
null=False,
verbose_name="댓글 권한",
default=0b011111110,
)
is_readonly = models.BooleanField(
verbose_name="읽기 전용 게시판",
help_text="활성화했을 때 관리자만 글을 쓸 수 있습니다. (ex. 포탈공지)",
default=False,
help_text="활성화했을 때 관리자만 글을 쓸 수 있습니다. (ex. 포탈공지)",
)
is_hidden = models.BooleanField(
verbose_name="리스트 숨김 게시판",
help_text="활성화했을 때 메인페이지 상단바 리스트에 나타나지 않습니다. (ex. 뉴아라공지)",
default=False,
db_index=True,
default=False,
help_text="활성화했을 때 메인페이지 상단바 리스트에 나타나지 않습니다. (ex. 뉴아라공지)",
)

name_type = models.SmallIntegerField(
verbose_name="닉네임/익명/실명글 허용 여부 설정",
help_text="글과 댓글을 어떤 이름 설정(닉네임/익명/실명)으로 작성할 수 있는지 정의합니다.",
default=NameType.REGULAR,
db_index=True,
default=NameType.REGULAR,
help_text="글과 댓글을 어떤 이름 설정(닉네임/익명/실명)으로 작성할 수 있는지 정의합니다.",
)
is_school_communication = models.BooleanField(
verbose_name="학교와의 소통 게시판",
help_text="학교 소통 게시판 글임을 표시",
default=False,
db_index=True,
default=False,
help_text="학교 소통 게시판 글임을 표시",
)
group_id = models.IntegerField(
verbose_name="그룹 ID",
default=1,
)
group_id = models.IntegerField(verbose_name="그룹 ID", default=1)
banner_image = models.ImageField(
default="default_banner.png",
upload_to="board_banner_images",
verbose_name="게시판 배너 이미지",
upload_to="board_banner_images",
default="default_banner.png",
)
ko_banner_description = models.TextField(
null=True,
verbose_name="게시판 배너에 삽입되는 국문 소개",
blank=True,
null=True,
default=None,
verbose_name="게시판 배너에 삽입되는 국문 소개",
)
en_banner_description = models.TextField(
null=True,
verbose_name="게시판 배너에 삽입되는 영문 소개",
blank=True,
null=True,
default=None,
verbose_name="게시판 배너에 삽입되는 영문 소개",
)
banner_url = models.TextField(
null=True,
verbose_name="게시판 배너를 클릭 시에 이동하는 링크",
blank=True,
null=True,
default=None,
verbose_name="게시판 배너를 클릭 시에 이동하는 링크",
)
top_threshold = models.SmallIntegerField(
verbose_name="인기글 달성 기준 좋아요 개수",
default=10,
)

class Meta(MetaDataModel.Meta):
verbose_name = "게시판"
verbose_name_plural = "게시판 목록"
unique_together = (
("ko_name", "deleted_at"),
("en_name", "deleted_at"),
)

def __str__(self) -> str:
return self.ko_name
Expand Down
14 changes: 14 additions & 0 deletions apps/core/views/viewsets/article.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,17 @@ def recent(self, request, *args, **kwargs):
[v for v in queryset], many=True, context={"request": request}
)
return self.paginator.get_paginated_response(serializer.data)

@decorators.action(detail=False, methods=["get"])
def top(self, request):
# The most recent article at the top
top_articles = Article.objects.exclude(topped_at__isnull=True).order_by(
"-topped_at", "-pk"
)
page = self.paginate_queryset(top_articles)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(top_articles, many=True)
return response.Response(data=serializer.data, status=status.HTTP_200_OK)
Loading

0 comments on commit 5d8937a

Please sign in to comment.