Skip to content

Commit

Permalink
✨Feat : 게시물 목록 API 구현 #9
Browse files Browse the repository at this point in the history
  • Loading branch information
simseulnyang committed Oct 26, 2023
1 parent 840817b commit 70f6b12
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 9 deletions.
2 changes: 2 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@
],
"DEFAULT_RENDERER_CLASSES": ("djangorestframework_camel_case.render.CamelCaseJSONRenderer",),
"DEFAULT_PARSER_CLASSES": ("djangorestframework_camel_case.parser.CamelCaseJSONParser",),
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 10,
}


Expand Down
9 changes: 6 additions & 3 deletions posts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# Generated by Django 4.2.6 on 2023-10-25 07:12
# Generated by Django 4.2.6 on 2023-10-27 00:14

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
('users', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
Expand All @@ -29,6 +31,7 @@ class Migration(migrations.Migration):
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_id', models.UUIDField(default=uuid.uuid4, editable=False)),
('post_type', models.CharField(choices=[('facebook', 'Facebook'), ('twitter', 'Twitter'), ('instagram', 'Instagram'), ('threads', 'Threads')], max_length=16)),
('title', models.CharField(max_length=32)),
('content', models.TextField()),
Expand All @@ -38,7 +41,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hashtag', models.ManyToManyField(related_name='posts', to='posts.hashtag')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'posts',
Expand Down
3 changes: 3 additions & 0 deletions posts/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import uuid

from django.db import models

from users.models import User
Expand All @@ -10,6 +12,7 @@ class PostType(models.TextChoices):
INSTAGRAM = "instagram"
THREADS = "threads"

content_id = models.UUIDField(default=uuid.uuid4, editable=False)
post_type = models.CharField(max_length=16, choices=PostType.choices)
title = models.CharField(max_length=32)
content = models.TextField()
Expand Down
20 changes: 20 additions & 0 deletions posts/paginations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class PaginationHandlerMixin(object):
@property
def paginator(self):
if not hasattr(self, "_paginator"):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
else:
pass
return self._paginator

def paginate_queryset(self, queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)

def get_paginated_response(self, data):
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
30 changes: 30 additions & 0 deletions posts/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from rest_framework import serializers

from .models import HashTag, Post


class StatisticsQuerySerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["date", "hour"])
Expand All @@ -12,3 +14,31 @@ class StatisticsQuerySerializer(serializers.Serializer):
class StatisticsListSerializer(serializers.Serializer):
datetime = serializers.DateTimeField()
count = serializers.IntegerField()


class HashTagSerializer(serializers.ModelSerializer):
class Meta:
model = HashTag
fields = [
"name",
]


class PostListSerializer(serializers.ModelSerializer):
hashtag = HashTagSerializer(many=True, read_only=True)

class Meta:
model = Post
fields = [
"content_id",
"post_type",
"title",
"content",
"view_count",
"like_count",
"share_count",
"created_at",
"updated_at",
"hashtag",
"user",
]
8 changes: 8 additions & 0 deletions posts/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from rest_framework.test import APITestCase


class PostListViewTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls
return super().setUpTestData()()
3 changes: 2 additions & 1 deletion posts/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.urls import path

from posts.views import StatisticsListView
from posts.views import PostListView, StatisticsListView

urlpatterns = [
path("statistics/", StatisticsListView.as_view(), name="statistics"),
path("list/", PostListView.as_view(), name="list"),
]
110 changes: 107 additions & 3 deletions posts/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
from datetime import datetime, timedelta

from django.db.models import Count, Q, Sum
from django.db.models import Count, F, Q, Sum
from django.db.models.functions import TruncDay, TruncHour
from django.db.models.query import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from common.dacorator import mandatories, optionals
from common.decorator import mandatories, optionals
from common.exceptions import InvalidParameterException, UnknownServerErrorException
from common.utils import get_before_week, get_now
from posts.models import Post
from posts.serializers import StatisticsListSerializer, StatisticsQuerySerializer
from posts.paginations import PaginationHandlerMixin
from posts.serializers import PostListSerializer, StatisticsListSerializer, StatisticsQuerySerializer


class StatisticsListView(APIView):
Expand Down Expand Up @@ -104,3 +107,104 @@ def get_statistics(self, queryset: Post, aggregation_field: str, value: str, agg
else:
raise InvalidParameterException("value는 count, view_count, share_count, like_count 중 선택 가능합니다.")
return statistics


class PostListView(PaginationHandlerMixin, APIView):
# @TODO: IsAuthenticated로 변경 @simseulnyang
permission_classes = [AllowAny]
filter_backends = [DjangoFilterBackend]
pagination_class = LimitOffsetPagination

@swagger_auto_schema(
operation_summary="게시물 리스트를 조회",
query_serializer=PostListSerializer,
responses={
status.HTTP_200_OK: PostListSerializer,
},
)
@mandatories("type")
@optionals({"search": "search_by"}, {"ordering": "orderby"}, {"hashtag": None})
def get(self, request: Request, m: dict, o: dict) -> Response:
"""
query parameter로 type, search, ordering, hashtag를 받아 게시물 목록을 조회합니다.
Args:
type: 게시물 타입으로 facebook, twitter, instagram, threads 중에 1개를 선택하여 조회 가능합니다. (default : 모든 게시물 타입)
search: title, content, title + content 검색이 가능합니다.
ordering: created_at, updated_at, view_count, share_count, like_count 기준으로 목록을 정렬합니다. (default: created_at)
hashtag: 조회할 해시태그입니다. (default: 본인계정)
Returns:
content_id : 게시물 id
hashtag : 해시태그
user : 게시글 작성 유저
post_type : 게시물 타입
title : 게시글 제목
content : 게시글 내용
view_count : 조회수
like_count : 좋아요 수
share_count : 공유 수
created_at : 작성일자
updated_at : 업데이트 일자
"""
try:
# 쿼리 매개변수 받기
post_type = m["type"]
search_query = o.get("search", "")
ordering = o.get("ordering", "created_at")
hashtag = request.user.username if o["hashtag"] is None else o["hashtag"]

# 유저 계정의 해시태그로 초기 필터링한 게시물 목록 가져오기
postlist = Post.objects.filter(user__username=hashtag)

# 검색어로 필터링
if search_query:
postlist = postlist.filter(Q(title__icontains=search_query) | Q(content__icontains=search_query))

# 정렬 기준 적용
postlist = self.apply_ordering(postlist, ordering)

# post_type에 따라 필터링 된 게시물 목록 가져오기
post_type_list = self.get_post_type_list(postlist, post_type)

# 게시물 목록 serialize
serializer = PostListSerializer(post_type_list, many=True)
except Exception as e:
raise UnknownServerErrorException(e)
return Response(serializer.data, status=status.HTTP_200_OK)

def apply_ordering(self, postlist, ordering):
"""
정렬 기준을 적용하여 postlist 정렬합니다.
ordering에 "asc" 또는 "desc"를 추가하여 오름차순 또는 내림차순 정렬 가능합니다.
Args:
postlist: 정렬할 postlist
ordering: 정렬 기준과 방향 (예: created_at:desc)
Returns:
정렬된 postlist
"""
ordering_parts = ordering.split(":")
field_name = ordering_parts[0]
direction = ordering_parts[1] if len(ordering_parts) > 1 else "desc"

if direction == "asc":
field_name = F(field_name).asc()
else:
field_name = F(field_name).desc()

return postlist.order_by(field_name)

def get_post_type_list(self, postlist: Post, post_type: str):
if post_type == "facebook":
post_type_list = postlist.filter(post_type="facebook")
elif post_type == "twitter":
post_type_list = postlist.filter(post_type="twitter")
elif post_type == "instagram":
post_type_list = postlist.filter(post_type="instagram")
elif post_type == "threads":
post_type_list = postlist.filter(post_type="threads")
else:
raise InvalidParameterException("post_type 값을 잘못 선택하셨습니다.")
return post_type_list
6 changes: 4 additions & 2 deletions users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by Django 4.2.6 on 2023-10-25 07:12
# Generated by Django 4.2.6 on 2023-10-27 00:14

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion

Expand All @@ -17,6 +18,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('username', models.CharField(max_length=128, unique=True)),
('email', models.EmailField(max_length=128, unique=True)),
('password', models.CharField(max_length=128)),
('is_confirmed', models.BooleanField(default=False)),
Expand All @@ -35,7 +37,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=32)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'user_confirm_codes',
Expand Down
1 change: 1 addition & 0 deletions users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


class User(AbstractBaseUser):
username = models.CharField(max_length=128, unique=True)
email = models.EmailField(max_length=128, unique=True)
password = models.CharField(max_length=128)
is_confirmed = models.BooleanField(default=False)
Expand Down

0 comments on commit 70f6b12

Please sign in to comment.