DRF를 이용하여 간단한 블로그를 설계한다.
URL | 기능 | 비회원 접근 | 회원 접근 | 작성자 접근 |
---|---|---|---|---|
/notice | 자유 게시판 | R | R, C | - |
/notice/int:post_pk | 자유 게시물 상세보기 | R | R | R, U, D |
/blog | 회원 게시판 | - | R, C | - |
/blog/int:post_pk | 회원 게시물 상세보기 | - | R | R, U, D |
기능 설명:
mkdir drf
cd drf
python -m venv venv
# source venv/Scripts/activate
.\venv\Scripts\activate
pip install django
pip install djangorestframework
django-admin startproject drf_tutorial .
python manage.py startapp notice
python manage.py startapp blog
python manage.py migrate
python manage.py createsuperuser
# drf_tutorial > settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third party apss
"rest_framework",
# my apps
"notice",
"blog",
]
# ... 중략 ...
LANGUAGE_CODE = 'ko-kr'
TIME_ZONE = 'Asia/Seoul'
USE_I18N = True
USE_TZ = True
# ... 중략 ...
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# blog > models.py
from django.db import models
from django.conf import settings
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
blog앱과 마찬가지로 같은 모델을 notice앱에도 생성한다. 클래스명만 Post와 Notice로 다르다.
# blog > serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__' # Test에서 사용하기 위해 모든 필드를 사용하도록 설정
notice앱도 마찬가지로 정의하고 import하는 이름만 다르다.
from django.contrib import admin
from .models import Post
admin.site.register(Post)
앞과 비슷하다.
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
접속한 후 후에 테스트를 위해 게시물을 3개 생성한다. 이후, 서버를 종료한 후 아직 정의안한 urls와 views를 정의한다.
# blog > urls.py
from django.urls import path
from . import views
urlpatterns = [
path('post/', views.post_list, name='post_list'),
path('post/<int:pk>/', views.post_detail, name='post_detail'),
]
# blog > views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework import status
from .models import Post
from .serializers import PostSerializer
@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def post_list(request):
if request.method == "GET":
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
elif request.method == "POST":
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(["GET", "PUT", "DELETE"])
@permission_classes([IsAuthenticated, IsAuthenticatedOrReadOnly])
def post_detail(request, pk):
try:
post = Post.objects.get(pk=pk)
except Post.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == "GET":
serializer = PostSerializer(post)
return Response(serializer.data)
elif request.method == "PUT":
if post.author != request.user:
return Response(status=status.HTTP_403_FORBIDDEN)
serializer = PostSerializer(post, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == "DELETE":
if post.author != request.user:
return Response(status=status.HTTP_403_FORBIDDEN)
post.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
DRF에 익숙하지 않으므로 먼저, FBV로 정의해주었다.
IsAuthenticated
: 로그인한 사용자만 접근 가능IsAuthenticatedOrReadOnly
: 게시글 상세보기(post_detail)는 로그인 여부와 상관없이 가능하지만, 수정/삭제는 작성자만 가능post_list
함수GET
요청: 모든 Post 객체를 조회하며 인증된 사용자만 접근할 수 있다.POST
요청: 새로운 Post 객체를 생성한다. 인증된 사용자만 접근할 수 있으며, author
필드에 현재 사용자를 자동으로 설정한다.post_detail
함수GET
요청: 특정 Post 객체를 조회한다. 인증된 사용자와 인증되지 않은 사용자 모두 접근할 수 있다.PUT
요청: 특정 Post 객체를 수정한다. 인증된 사용자 중에서도 해당 Post의 author
인 경우에만 접근할 수 있다.DELETE
요청: 특정 Post 객체를 삭제한다. 인증된 사용자 중에서도 해당 Post의 author
인 경우에만 접근할 수 있다.from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework import status
from .models import Notice
from .serializers import NoticeSerializer
@api_view(["GET", "POST"])
def notice_list(request):
if request.method == "GET":
notices = Notice.objects.all()
serializer = NoticeSerializer(notices, many=True)
return Response(serializer.data)
elif request.method == "POST":
if not request.user.is_authenticated:
return Response(status=status.HTTP_401_UNAUTHORIZED)
serializer = NoticeSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(["GET", "PUT", "DELETE"])
@permission_classes([IsAuthenticatedOrReadOnly])
def notice_detail(request, pk):
try:
notice = Notice.objects.get(pk=pk)
except Notice.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == "GET":
serializer = NoticeSerializer(notice)
return Response(serializer.data)
elif request.method == "PUT":
if not request.user.is_authenticated or notice.author != request.user:
return Response(status=status.HTTP_403_FORBIDDEN)
serializer = NoticeSerializer(notice, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == "DELETE":
if not request.user.is_authenticated or notice.author != request.user:
return Response(status=status.HTTP_403_FORBIDDEN)
notice.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
notice_list
함수GET
요청: 인증 여부와 상관없이 모든 사용자가 접근할 수 있다.POST
요청: 인증된 사용자만 접근할 수 있으며, author
필드에 현재 사용자를 자동으로 설정한다. 인증되지 않은 사용자가 접근하면 HTTP_401_UNAUTHORIZED
응답을 반환한다.notice_detail
함수GET
요청: 인증된 사용자와 인증되지 않은 사용자 모두 접근할 수 있다.PUT
요청: 인증된 사용자 중에서도 해당 Notice의 author
인 경우에만 접근할 수 있다. 그렇지 않은 경우 HTTP_403_FORBIDDEN
응답을 반환한다.DELETE
요청: 인증된 사용자 중에서도 해당 Notice의 author
인 경우에만 접근할 수 있다. 그렇지 않은 경우 HTTP_403_FORBIDDEN
응답을 반환한다.thunder client로 테스트를 해 보았다.
GET
요청: 비회원도 접근 가능하므로 200(OK) 응답POST
요청 (비회원): 비회원은 작성 권한이 없으므로 401(Unauthorized) 응답POST
요청 (회원):{"title": "test title", "content": "test content", "author": 1}
package.json
은 Thunder Client에서 요청 본문(request body)을 저장하는 데 사용된다.
thunder client로 테스트 했던 것을 코드 형태로 짜보았다. 보통의 경우 이런 테스트 코드는 직접 코드를 짜기 전에 작성한다.
규모가 어느정도 커지면 테스트 코드의 장점이 극대화되며 작은 경우는 안쓰는게 좋을 수 있다.
# blog > tests.py
from django.test import TestCase
from rest_framework.test import APIClient
from django.contrib.auth.models import User
from blog.models import Post
class BlogTest(TestCase):
def setUp(self):
print("-- main app 테스트 BEGIN --")
self.client = APIClient()
self.user = User.objects.create_user(
username="hojun",
password="dlghwns1234!",
)
self.user.save()
self.blog = Post.objects.create(
title="test blog title setup",
content="test blog content setup",
author=self.user,
)
self.blog.save()
print("-- main app 테스트 END --")
def test_blog_read(self):
"""
blog list Read 가능 테스트
"""
print("-- blog read 테스트 BEGIN --")
print("-- 비회원 읽기 테스트 --")
response = self.client.get("/blog/post/")
self.assertEqual(response.status_code, 403)
print("-- 회원 읽기 테스트 --")
self.client.login(username="hojun", password="dlghwns1234!")
response = self.client.get("/blog/post/")
self.assertEqual(response.status_code, 200)
print("--// blog read 테스트 END --")
def test_blog_create(self):
"""
blog Create 가능 테스트
"""
print("-- blog create 테스트 BEGIN --")
print("-- 비회원 작성 테스트 --")
response = self.client.post(
"/blog/post/",
{
"title": "test blog title create",
"content": "test blog content create",
"author": self.user.id,
},
format="json",
)
self.assertEqual(response.status_code, 403)
print("-- 회원 작성 테스트 --")
self.client.login(username="hojun", password="dlghwns1234!")
response = self.client.post(
"/blog/post/",
{
"title": "test blog title create",
"content": "test blog content create",
"author": self.user.id,
},
format="json",
)
self.assertEqual(response.status_code, 201)
posts = Post.objects.all()
for i in posts:
print(i.title)
print("--// blog create 테스트 END --")
setUp()
메서드에서 테스트에 필요한 사용자와 블로그 게시글을 미리 생성하여 초기화한다.
APIClient
는 실제 HTTP 요청을 보내고 응답을 받을 수 있다.
마지막의 반복문은 명확하게 보기 위함이다.
# notice > tests.py
from django.test import TestCase
from rest_framework.test import APIClient
from django.contrib.auth.models import User
from notice.models import Notice as Post
class NoticeTest(TestCase):
def setUp(self):
print("-- main app 테스트 BEGIN --")
self.client = APIClient()
self.user = User.objects.create_user(
username="hojun",
password="dlghwns1234!",
)
self.user.save()
self.notice = Post.objects.create(
title="test notice title setup",
content="test notice content setup",
author=self.user,
)
self.notice.save()
print("-- main app 테스트 END --")
def test_notice_read(self):
"""
notice list Read 가능 테스트
"""
print("-- notice read 테스트 BEGIN --")
print("-- 비회원 읽기 테스트 --")
response = self.client.get("/notice/post/")
self.assertEqual(response.status_code, 200)
def test_notice_create(self):
"""
notice Create 가능 테스트
"""
print("-- notice create 테스트 BEGIN --")
print("-- 비회원 작성 테스트 --")
response = self.client.post(
"/blog/post/",
{
"title": "test blog title create",
"content": "test blog content create",
"author": self.user.id,
},
format="json",
)
self.assertEqual(response.status_code, 403)
print("-- 회원 작성 테스트 --")
self.client.login(username="hojun", password="dlghwns1234!")
response = self.client.post(
"/blog/post/",
{
"title": "test blog title create",
"content": "test blog content create",
"author": self.user.id,
},
format="json",
)
self.assertEqual(response.status_code, 201)
posts = Post.objects.all()
for i in posts:
print(i.title)
print("--// notice create 테스트 END --")