게시글&댓글 관련 기초 api 설계를 해보자!
from django.urls import path
from articles import views
urlpatterns = [
path('', views.ArticleView.as_view(), name='article_view'),
path('<int:article_id>/', views.ArticleDetailView.as_view(), name='article_detail_view'),
path('comment/', views.CommentView.as_view(), name='comment_view'),
path('comment/<int:comment_id>/', views.CommentDetailView.as_view(), name='comment_detail_view'),
path('like/', views.LikeView.as_view(), name='like_view'),
]
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
class ArticleView(APIView):
def get(self, request):
pass
def post(self, request):
pass
class ArticleDetailView(APIView):
def get(self, request, article_id):
pass
def put(self, request, article_id):
pass
def delete(self, request, article_id):
pass
class CommentView(APIView):
def get(self, request):
pass
def post(self, request):
pass
class CommentDetailView(APIView):
def put(self, request, comment_id):
pass
def delete(self, request, comment_id):
pass
class LikeView(APIView):
def post(self, request):
pass
게시글 리스트 보여주기, 게시글 작성하기, 게시글 상세페이지 보여주기, 게시글 상세페이지에서 수정하기, 게시글 상세페이지에서 삭제하기, 댓글 전체 보여주기, 댓글 작성하기, 댓글 수정하기, 댓글 삭제하기, 좋아요에 대한 api들을 작성!!
from django.db import models
from user.models import User
class Article(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length = 50)
content = models.TextField()
image = models.ImageField(blank=True, upload_to='%Y/%m/')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now = True)
def __str__(self):
return str(self.title)
여기서 upload_to='%Y/%m/' 는 사용자가 사진을 업로드 하였을 때 미디어 폴더 안에 년/월 폴더를 자동으로 생성하여 그 안에 사진을 보관한다.
장고 스테틱 관련 자료 : https://docs.djangoproject.com/en/4.1/howto/static-files/
웹사이트에는 일반적으로 이미지, 자바스크립트, css같은 추가적인 파일들이 필요한데 이것들을 static files 이라고 한다.
사용자의 요청에 따라 내용이 바뀌는 것이 아니라 요청한것을 그대로 내어주면 되는 파일들이다!
스테틱 파일-> 관리자가 올린 파일
미디어 파일-> 사용자들이 올린 파일
STATIC_ROOT = BASE_DIR / "static"
STATIC_URL = "static/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
이 코드를 setting.py에 추가
from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
이 코드를 프로젝트의 urls.py에 추가
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from articles.models import Article
from articles.serializers import ArticleListSerializer
class ArticleView(APIView):
def get(self, request):
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
from rest_framework import serializers
from articles.models import Article
class ArticleListSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField() # user의 벨류값이 pk값으로 나오는것을 email로 불러내기 위해 메소드시리얼라이저 사용
def get_user(self, obj):
return obj.user.email
class Meta:
model = Article
fields = ("pk", "title", "image", "updated_at", "user") # 원하는 필드들을 고를 수 있음
원하는 필드들만 게시글 리스트에 보여주기 위해 fields에서 직접 선정해 주었다.
또한 user의 값이 pk형태로 나와서 email형태로 나오게 해주기 위해 메소드시리얼라이저를 사용하였다.
아티클의 오브젝트의 유저의 이메일을 리턴값으로 돌려주고 이 값이 user로 들어가서 화면에 email이 뜨게 되는 형식이다.
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from articles import serializers
from articles.models import Article
from articles.serializers import ArticleListSerializer, ArticleSerializer, ArticleCreateSerializer
class ArticleView(APIView):
def post(self, request):
serializer = ArticleCreateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.data)
else:
return Response(serializer.errors)
이 코드를 views.py에 추가
from rest_framework import serializers
from articles.models import Article
class ArticleCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("title", "image", "content")
이 코드를 serializers.py에 추가
class ArticleDetailView(APIView):
def get(self, request, article_id):
article = Article.objects.get(id=article_id)
serializer = ArticleSerializer(article)
return Response(serializer.data, status=status.HTTP_200_OK)
class ArticleSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.email
class Meta:
model = Article
fields = "__all__"
이 또한 user를 email형태로 부르기 위해
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.email
코드를 추가해주었다.
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from articles import serializers
from articles.models import Article
from articles.serializers import ArticleListSerializer, ArticleSerializer, ArticleCreateSerializer
from rest_framework.generics import get_object_or_404
class ArticleDetailView(APIView):
def put(self, request, article_id):
article = get_object_or_404(Article, id=article_id)
if request.user == article.user:
serializer = ArticleCreateSerializer(article, data = request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
게시글 수정 기능과 본인이 아닐경우 삭제를 할 수 없도록 하는 기능을 추가하였다!!
class ArticleDetailView(APIView):
def delete(self, request, article_id):
article = get_object_or_404(Article, id=article_id)
if request.user == article.user:
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
else:
return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
게시글 삭제 기능, 본인이 아니면 삭제할 수 없게하는 기능을 추가
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comment_set") # 역으로 참조할 때에는 related_name 사용, related_name="comment_set"은 디폴트 값이어서 작성 안해줘도 있는것으로 인식된다.
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now = True)
def __str__(self):
return str(self.content)
_set 또는 related_name 사용
아무것도 작성을 안했을 때 디폴트 값은 comment_set형태이다.
ex) 나를 바라보고 있는 것_set(comment_set)
_set를 대신하여 사용할 수 있는것으로 이름을 직관적으로 작성할 수 있다.
related_name이 꼭 필요한 경우가 있는데 이는 한 클래스에서 서로 다른 두 column이 같은 table을 참조하는 경우이다.
예) comment와 user 모두 참조하는 경우
참고 자료 : https://velog.io/@hj8853/Django-ManyToMany-relatedname
class CommentView(APIView):
def get(self, request, article_id):
article = Article.objects.get(id = article_id)
comments = article.comment_set.all()
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, article_id):
serializer = CommentCreateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, article_id=article_id)
return Response(serializer.data)
else:
return Response(serializer.errors)
댓글 보이기, 댓글 작성하기 코드 작성
class CommentDetailView(APIView):
def put(self, request, article_id, comment_id):
comment = get_object_or_404(Comment, id=comment_id)
if request.user == comment.user:
serializer = CommentCreateSerializer(comment, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
def delete(self, request, comment_id, article_id):
comment = get_object_or_404(Comment, id=comment_id)
if request.user == comment.user:
comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
else:
return Response("권한이 없습니다!", status=status.HTTP_403_FORBIDDEN)
댓글 수정하기, 댓글 삭제하기 코드 추가
class CommentSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.email
class Meta:
model = Comment
fields= "__all__"
class CommentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ("content",) # 들어있는데 핃드가 1개라도 콤마 필수
fields안에 1개의 변수만 들어갈 때도 끝에 꼭 ,를 붙여줘야 한다.
fields(복수형)이라는 조건이 있기 때문에!!(안쓰면 string으로 인식한다!)
class Article(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length = 50)
content = models.TextField()
image = models.ImageField(blank=True, upload_to='%Y/%m/')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now = True)
likes = models.ManyToManyField(User, related_name="like_articles") #manytomany 필드는 related_name을 설정안하면 이름이 중복되기 때문에 꼭 설정을 해주어야 한다!!
def __str__(self):
return str(self.title)
위에서 설명한 것 처럼 foreignkey에서 이미 역참조를 하고 있는 상황에서 아무런 설정 없이 참조를 또 하게되면 오류가 발생한다.
때문에 likes에서 역참조 할 때에는 related_name을 사용하여 like_articles라고 이름을 정해주었다.
class LikeView(APIView):
def post(self, request, article_id):
article = get_object_or_404(Article, id=article_id)
if request.user in article.likes.all():
article.likes.remove(request.user)
return Response("좋아요 취소", status=status.HTTP_200_OK)
else:
article.likes.add(request.user)
return Response("좋아요", status=status.HTTP_200_OK)
request.user가 게시글에 이미 좋아요를 눌렀으면 request.user를 좋아요 목록에서 빼고, 아니면 좋아요 목록에 추가해라 라는 코드를 추가하였다.
followings = models.ManyToManyField('self', symmetrical=False, related_name='followers') # symmetical은 대칭인지 아닌지!
User모델에 이 코드를 추가하였다.
이 또한 ManyToMany필드 역참조라 related_name 추가해주고, symmetrical이라는 대칭인지 아닌지를 구분해주는 것을 추가해주었다.
인스타그램의 경우 일방적으로 팔로우가 가능하므로 대칭형태가 아니기 때문에 False로 값을 주었다.
class FollowView(APIView):
def post(self, request, user_id):
you = get_object_or_404(User, id=user_id)
me = request.user
if me in you.followers.all():
you.followers.remove(me)
return Response("언팔로우", status=status.HTTP_200_OK)
else:
you.followers.add(me)
return Response("팔로우", status=status.HTTP_200_OK)
좋아요에서 쓴 구문을 동일하게 작성해주었다.
class ArticleListSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
likes_count = serializers.SerializerMethodField()
comment_set_count = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.email
def get_likes_count(self, obj):
return obj.likes.count()
def get_comment_set_count(self, obj):
return obj.comment_set.count()
class Meta:
model = Article
fields = ("pk", "title", "image", "updated_at", "user", "likes_count", "comment_set_count")
class CommentSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
def get_user(self, obj):
return obj.user.email
class Meta:
model = Comment
exclude = ("article",)
user가 이메일 형태로 나오게 설정되어 있고, article필드를 제외하기 위해서 exclude를 사용하였다.
이 또한 필드가 1개뿐이더라도 끝에 꼭 ,를 붙여야 한다!
class ArticleSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
comment_set = CommentSerializer(many=True)
likes = serializers.StringRelatedField(many=True)
def get_user(self, obj):
return obj.user.email
class Meta:
model = Article
fields = "__all__"
path('<int:user_id>/', views.ProfileView.as_view(), name='profile_view'),
class ProfileView(APIView):
def get(self, request, user_id):
user = get_object_or_404(User, id=user_id)
serializer = UserProfileSerializer(user)
return Response(serializer.data)
class UserProfileSerializer(serializers.ModelSerializer):
# followers = serializers.PrimaryKeyRelatedField(many=True, read_only=True) followers와 followings가 pk(id)값으로 들어가게 하는 코드
followers = serializers.StringRelatedField(many=True) # 이메일로 보이게
followings = serializers.StringRelatedField(many=True) # 이베일로 보이게
article_set = ArticleListSerializer(many=True) # 본인 게시글 모두 불러오기
like_articles = ArticleListSerializer(many=True) # 좋아요한 게시글 모두 불러오기
class Meta:
model = User
fields=("id", "email", "followings", "followers", "article_set", "like_articles")
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = ('id', 'email', 'is_admin')
list_filter = ('is_admin',)
fieldsets = (
(None, {'fields': ('email', 'password', 'followings')}),
('Permissions', {'fields': ('is_admin',)}),
)
Q-object 관련자료 : https://docs.djangoproject.com/en/4.1/topics/db/queries/#complex-lookups-with-q
path('feed/', views.FeedView.as_view(), name='feed_view'),
from django.db.models.query_utils import Q
class FeedView(APIView):
permission_classes = [permissions.IsAuthenticated] # 로그인이 되어있는지
def get(self, request):
q = Q()
for user in request.user.followings.all(): # 로그인한 유저가 팔로잉하고있는 모든 유저들을 for문으로 돌리겠다.
q.add(Q(user=user), q.OR) # user가 내가 팔로잉하고 있는 유저일 때 그 유저의 값들을 모두 q에 넣는다.(or이기 때문에)
feeds = Article.objects.filter(q) # feeds는 내가 팔로잉하고 있는 유저들의 피드들이다.
serializer = ArticleListSerializer(feeds, many=True) # 그 feeds를 모두 불러오겠다!
return Response(serializer.data)
쿼리에 or나 and를 적용시킬 수 있는 Q를 사용하였다.
user가 내가 팔로잉하고 있는 유저일 때의 값을 모두 q에 넣고 그 q값을 feeds의 코드에 넣으면 팔로잉하고 있는 유저의 모든 게시물들이 불려온다!!