django-restframework
에 대해서 이해하고 적절히 활용할 수 있다.TDD(Test Driven Development
방식에 대해서 이해하고 수행할 수 있다.Django ORM
을 사용하여 데이터베이스 모델을 구성할 수 있다.Django Test
에 대해서 이해하고 테스트 코드를 적절히 작성할 수 있다.Serializer
에 대해서 이해하고 활용할 수 있다.Viewset
에 대해서 이해하고 적절히 활용할 수 있다.
지난 과제에서는 original django를 이용하여 To Do List를 만들어 보았습니다.
이번 과제부터 마지막 챕터까지 TDD 방식과 DRF를 사용하여 맛집 리뷰 웹사이트를 구현하고 배포해보도록 하겠습니다.
아래의 요구사항을 참고하여 프로젝트를 구성하고 Viewset을 이용한 기본적인 CRUD를 구성하세요.
Poetry
가상환경을 이용합니다.django
, django-restframework
를 설치합니다.pillow
, django-cleanup
라이브러리를 추가로 설치합니다.django-admin startproject config .
명령어를 사용하여 장고 프로젝트를 세팅합니다.python manage.py startapp
명령어를 사용하여 users, restaurants, reviews 앱을 각각 구성합니다.아래의 요구사항을 참고하여 데이터베이스 모델을 구성하세요.
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.db import models
class UserManager(BaseUserManager):
def create_user(self, email, password, nickname,*args, **kwargs):
if not email or not nickname:
raise ValueError("이메일, 닉네임을 입력하세요")
user = self.model(
email=self.normalize_email(email),
nickname=nickname,*args, **kwargs
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password, nickname):
user = self.create_user(email, password, nickname)
user.is_superuser = True
user.is_staff = True
user.save(using=self._db)
return user
# Create your models here.
class User(AbstractBaseUser, PermissionsMixin): # Permission : is_superuser
nickname = models.CharField('닉네임', max_length=20, unique=True)
email = models.CharField('이메일', max_length=40, unique=True)
profile_image = models.ImageField(upload_to='users/profile_image', default='users/blank_profile_image.png')
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['nickname']
def __str__(self):
return self.email
class Meta:
verbose_name = '유저'
verbose_name_plural = '유저 목록'
BaseModel
config / models.py 에 선언
fields
- created_at - DatetimeField(auto_now_add=True)
- modified_at - DatetimeField(auto_now=True)
from django.db import models
class BaseModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
Restaurant
DAYS_OF_WEEK = [
('MON', 'Monday'),
('TUE', 'Tuesday'),
('WED', 'Wednesday'),
('THU', 'Thursday'),
('FRI', 'Friday'),
('SAT', 'Saturday'),
('SUN', 'Sunday'),
]
from django.db import models
from config.models import BaseModel
# Create your models here.
class Restaurant(BaseModel):
DAYS_OF_WEEK = [
('MON', 'Monday'),
('TUE', 'Tuesday'),
('WED', 'Wednesday'),
('THU', 'Thursday'),
('FRI', 'Friday'),
('SAT', 'Saturday'),
('SUN', 'Sunday'),
]
name = models.CharField(max_length=50)
address = models.CharField(max_length=200)
contact = models.CharField(max_length=50)
open_time = models.TimeField(null=True, blank=True)
close_time = models.TimeField(null=True, blank=True)
last_order = models.TimeField(null=True, blank=True)
regular_holiday = models.CharField(choices=DAYS_OF_WEEK, null=True, blank=True, max_length=10)
def __str__(self):
return self.name
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import ForeignKey
from config.models import BaseModel
from restaurants.models import Restaurant
User = get_user_model()
# Create your models here.
class Review(BaseModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
comment = models.TextField()
def __str__(self):
return f"{self.restaurant.name} 리뷰"
아래의 요구사항을 참고하여 Model Test Code를 작성하세요.
users / tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
# Create your tests here.
User = get_user_model()
class UserModelTest(TestCase):
def setUp(self):
self.user = {
'email' : 'test@test.com',
'nickname' :'test',
'password' : 'password1234'
}
self.super_user = {
'email': 'admin@example.com',
'nickname': 'admin_user',
'password': 'password1234',
}
def test_user_manager_create_user(self):
user = User.objects.create_user(**self.user)
self.assertEqual(User.objects.count(), 1)
self.assertEqual(user.email, self.user['email'])
self.assertFalse(user.is_staff, False)
self.assertFalse(user.is_superuser, False)
self.assertTrue(user.is_active, True)
def test_user_manager_create_superuser(self):
user = User.objects.create_superuser(**self.super_user)
self.assertEqual(User.objects.count(), 1)
self.assertTrue(user.is_staff, True)
self.assertTrue(user.is_active, True)
self.assertEqual(user.profile_image.url, '/media/users/blank_profile_image.png')
restaurants / tests.py
from django.test import TestCase
from restaurants.models import Restaurant
# Create your tests here.
class RestaurantTestCase(TestCase):
def setUp(self):
self.restaurant_info = {
'name' : 'test',
'address' :'주소',
'contact' :'없음',
'regular_holiday' :'MON'
}
def test_create_restaurant(self):
restaurant = Restaurant.objects.create(**self.restaurant_info)
self.assertEqual(Restaurant.objects.count(), 1)
self.assertEqual(restaurant.contact, '없음')
self.assertEqual(restaurant.address, self.restaurant_info['address'])
reviews / tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from restaurants.models import Restaurant
from reviews.models import Review
# Create your tests here.
class ReviewTestCase(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
nickname='test', email='test@example.com', password='password1234'
)
self.restaurant = Restaurant.objects.create(
name='Test Restaurant',
address='test',
contact='없음'
)
self.data = {
'user': self.user,
'restaurant': self.restaurant,
'title': 'test',
'comment': 'test'
}
def test_create_review(self):
review = Review.objects.create(**self.data)
self.assertEqual(review.title, self.data['title'])
self.assertEqual(review.comment, 'test')
self.assertEqual(review.user, self.data['user'])
self.assertEqual(review.restaurant, self.data['restaurant'])
TDD
방식을 따라 코드를 구현하기 위해서 View Test Code를 먼저 작성합니다.
아래의 요구사항을 참고하여 TestCode를 작성해주세요.
# restaurant/test.py
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from restaurants.models import Restaurant
# Create your tests here.
class RestaurantTestCase(TestCase):
def setUp(self):
self.restaurant_info = {
'name' : 'test',
'address' :'주소',
'contact' :'없음',
'regular_holiday' :'MON'
}
def test_create_restaurant(self):
restaurant = Restaurant.objects.create(**self.restaurant_info)
self.assertEqual(Restaurant.objects.count(), 1)
self.assertEqual(restaurant.contact, '없음')
self.assertEqual(restaurant.address, self.restaurant_info['address'])
class RestaurantViewTestCase(APITestCase):
def setUp(self):
self.restaurant_info = {
'name': 'test',
'address': '주소',
'contact': '없음',
'regular_holiday': 'MON'
}
def test_restaurant_list_view(self):
url = reverse('restaurant-list')
restaurant = Restaurant.objects.create(**self.restaurant_info)
# client.get -> 해당 엔드포인트로 GET요청을 보낸후 , JSON 형식으로 응답을 받는다.
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('results')[0].get('name'), 'test')
self.assertEqual(response.data.get('results')[0]['regular_holiday'], self.restaurant_info['regular_holiday'])
def test_restaurant_post_view(self):
# restaurant-create 는 존재하지 않음
url = reverse('restaurant-list')
response = self.client.post(url, data=self.restaurant_info, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Restaurant.objects.count(), 1)
self.assertEqual(response.data['name'], 'test')
def test_restaurant_detail_view(self):
restaurant = Restaurant.objects.create(**self.restaurant_info)
url = reverse('restaurant-detail', kwargs={'pk':restaurant.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], 'test')
self.assertEqual(response.data.get('name'), 'test')
def test_restaurant_update_view(self):
restaurant = Restaurant.objects.create(**self.restaurant_info)
url = reverse('restaurant-detail', kwargs={'pk':restaurant.pk})
response = self.client.patch(url, {'name':'patch'}, format='json')
self.assertEqual(response.data.get('name'),'patch')
def test_restaurant_delete_view(self):
restaurant = Restaurant.objects.create(**self.restaurant_info)
url = reverse('restaurant-detail', kwargs={'pk':restaurant.pk})
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Restaurant.objects.count(), 0)
rest_framework.serializers
에서 ModelSerializer
를 사용하여 Restaurant 모델에 대한 시리얼라이저를 구성합니다.
아래의 요구사항을 충족하는 serializers.py를 생성해주세요.
ModelSerializer
를 상속받아 사용from rest_framework import serializers
from restaurants.models import Restaurant
class RestaurantSerializer(serializers.ModelSerializer):
class Meta:
model = Restaurant
fields = '__all__'
rest_framework.viewsets
의 ModelViewSet
을 활용하세요.from django.shortcuts import render
# Create your views here.
from rest_framework import viewsets, permissions
from restaurants.models import Restaurant
from restaurants.serializers import RestaurantSerializer
class RestaurantViewSet(viewsets.ModelViewSet):
queryset = Restaurant.objects.all()
serializer_class = RestaurantSerializer
이번 프로젝트에서는 URL Include 방식을 사용하여 url 엔드포인트들을 구성하겠습니다.
아래의 요구사항을 충족하는 앱의 urls.py 생성하고 config / urls.py 에서 include 를 사용해주세요.
rest_framwork.routers
의 DefaultRouter
를 활용하면 ViewSet을 사용할 때 각각의 요청 메서드에 따라 아래의 url 엔드포인트를 생성해줍니다.restaurants/
restaurants/<int:pk>/
# restaurants/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from restaurants import views
router = DefaultRouter()
router.register(r'restaurants', views.RestaurantViewSet, basename='restaurant')
urlpatterns = [
path('', include(router.urls)),
]
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('restaurants.urls')),
]
django-restframework
에 대해서 이해하고 적절히 활용할 수 있다.TDD(Test Driven Development
방식에 대해서 이해하고 수행할 수 있다.Django ORM
을 사용하여 데이터베이스 모델을 구성할 수 있다.Django Test
에 대해서 이해하고 테스트 코드를 적절히 작성할 수 있다.Serializer
에 대해서 이해하고 활용할 수 있다.rest_framework.views
의API View
에 대해서 이해하고 적절히 활용할 수 있다.rest_framework.generics
의 각각의 View Class에 대해서 이해하고 적절히 활용할 수 있다.
지난 과제에서는 TDD 방식과 ViewSet를 이용하여 Restaurant 모델에 대한 url 을 구성해보았습니다.
이번 과제에서는 TDD 방식과 APIView를 이용하여 유저 회원가입 및 로그인, 로그아웃 기능을, GenericView를 이용하여 Review 기능을 간단하게 구현해보겠습니다.
아래의 요구사항을 참고하여 코드를 구성하세요.
REST_FRAMEWORK 설정에 아래의 요구사항을 추가해주세요.
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
]
}
TDD
방식을 따라 코드를 구현하기 위해서 View Test Code를 먼저 작성합니다.
아래의 요구사항을 참고하여 TestCode를 작성해주세요.
self.client.force_authenticate(user=self.user)
or setup 에서 login을 시켜주는방법# restaurants/tests.py
class RestaurantViewTestCase(APITestCase):
def setUp(self):
self.restaurant_info = {
'name': 'test',
'address': '주소',
'contact': '없음',
'regular_holiday': 'MON'
}
self.user = get_user_model().objects.create_user(
email='test@test.com',
nickname='test',
password='password1234',
)
# self.client.force_authenticate(user=self.user)
self.client.login(email='test@test.com', password='password1234')
# 필요한 곳에 적용시키거나
def test_restaurant_update_view(self):
restaurant = Restaurant.objects.create(**self.restaurant_info)
url = reverse('restaurant-detail', kwargs={'pk':restaurant.pk})
self.client.force_authenticate(user=self.user)
response = self.client.patch(url, {'name':'patch'}, format='json')
# users/tests.py
class UserAPIViewTestCase(APITestCase):
def setUp(self):
self.user = {
'email': 'test@test.com',
'nickname': 'test',
'password': 'password1234'
}
def test_user_signup(self):
url = reverse('user-signup')
response = self.client.post(url, self.user)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(User.objects.count(), 1)
self.assertEqual(response.data.get('nickname'), 'test')
def test_user_login(self):
user = User.objects.create_user(**self.user)
url = reverse('user-login')
data = {
'email' : self.user.get('email'),
'password': self.user.get('password'),
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('message'), 'login success')
def test_user_login_invalid_credentials(self):
user = User.objects.create_user(**self.user)
data = {
'email': 'test@test.com',
'password': 'wrong'
}
response = self.client.post(reverse('user-login'), data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_get_user_detail(self):
user = User.objects.create_user(**self.user)
url = reverse('user-detail', kwargs={'pk': user.pk})
# 로그인 시키는 위치 중요
self.client.login(email=self.user['email'], password=self.user['password'])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('email'), 'test@test.com')
def test_update_user_details(self):
user = User.objects.create_user(**self.user)
url = reverse('user-detail', kwargs={'pk': user.pk})
# 로그인 시키는 위치 중요
self.client.login(email=self.user['email'], password=self.user['password'])
data = {
'nickname' : 'update_test'
}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('nickname'), 'update_test')
def test_delete_user(self):
user = User.objects.create_user(**self.user)
url = reverse('user-detail', kwargs={'pk': user.pk})
# 로그인 시키는 위치 중요
self.client.login(email=self.user['email'], password=self.user['password'])
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(User.objects.count(), 0)
# reviews/tests.py
class ReviewAPIViewTestCase(APITestCase):
def setUp(self):
# 테스트 코드를 작성하기 위해 필요한 것들을 미리 생성해두는 것
self.user = get_user_model().objects.create_user(
nickname='test', email='test@example.com', password='password1234'
)
self.client.login(email='test@example.com', password='password1234')
self.restaurant = Restaurant.objects.create(
name='Test Restaurant',
address='test',
contact='없음'
)
self.data = {
'user': self.user,
'restaurant': self.restaurant,
'title': 'test',
'comment': 'test'
}
def test_get_review_list(self):
# 작성된 리뷰 리스트를 가져오기 위한 APIView 테스트를 위한 코드 작성
review = Review.objects.create(**self.data)
url = reverse('review-list-create', kwargs= {'restaurant_pk': review.restaurant.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Review.objects.count(), 1)
def test_post_review(self):
# 리뷰를 작성하면 데이터베이스에 저장되는 APIView 테스트를 위한 코드 작성
url = reverse('review-list-create', kwargs={'restaurant_pk': self.restaurant.pk})
data ={
'title':'test',
'comment':'test'
}
response = self.client.post(url, data, format='json')
print(response.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Review.objects.count(), 1)
def test_get_review_detail(self):
# 리뷰 상세 정보를 가져오기 위한 APIView 테스트를 위한 코드 작성
review = Review.objects.create(**self.data)
url = reverse('review-detail', kwargs={'review_pk': review.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('title'), 'test')
def test_update_review(self):
# 리뷰 제목, 내용을 업데이트하기 위한 APIView 테스트를 위한 코드 작성
review = Review.objects.create(**self.data)
url = reverse('review-detail', kwargs={'review_pk': review.pk})
data = {
'title' :'update'
}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('title'), 'update')
def test_delete_review(self):
# 리뷰를 삭제하는 APIView 테스트를 위한 코드 작성
review = Review.objects.create(**self.data)
url = reverse('review-detail', kwargs={'review_pk': review.pk})
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Review.objects.count(), 0)
self.assertFalse(Review.objects.filter(pk=review.pk).exists())
rest_framework.serializers
에서 ModelSerializer
를 사용하여 각 모델에 대한 시리얼라이저를 구성합니다.
아래의 요구사항에 따라 serializers.py 를 수정하거나 추가합니다.
ModelSerializer
를 시리얼라이저로 사용할 때, POST 메서드일 떄 이 함수가 호출됩니다.set_password()
를 사용하여 생성해주세요.class UserSerializer(serializers.ModelSerializer):
# 보안상의 이유로 클라이언트가 입력은 가능하지만 API응답에는 포함안시키기위해
password = serializers.CharField(write_only=True)
class Meta:
model = get_user_model()
fields = ('id', 'nickname', 'email', 'password','is_staff','is_superuser',)
# 비밀전호를 평문으로 저장하지 않고 해싱해서 저장하기 위해사용
def create(self, request):
# pop메소드는 제거한 값을 반환함
password = request.pop('password', None)
if not password:
raise serializers.ValidationError("패스워드를 입력하세요")
user = get_user_model().objects.create_user(**request)
user.set_password(password)
user.save()
return user
ModelSerializer
를 상속받아 사용ModelSerializer
를 시리얼라이저로 사용할 때, put 또는 patch 메서드로 요청이오면 update 메서드가 호출됩니다.set_password()
를 사용하여 업데이트 하도록 해당 메서드를 구성해주세요.class UserDetailSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = get_user_model()
fields = ['id,','nickname','email','password','profile_image']
READ_ONLY_FIELDS = ['id','email']
# 수정할 객체, 클라이언트가 보내는 형식
def update(self, instance, validated_data):
user = get_user_model().objects.get(pk=instance.pk)
password = validated_data.pop('password', None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
if password:
user.set_password(password)
instance.save()
return instance
Serializer
를 상속받아 사용Serializer
를 상속받아 시리얼라이저로 사용할 때 이 함수가 호출됩니다. 요청으로부터 받은 데이터를 검증하는 메서드입니다.# class UserLoginSerializer(serializers.Serializer):
# email = serializers.EmailField()
# password = serializers.CharField(write_only=True)
# def validate(self, attrs):
# email = attrs.get('email')
# password = attrs.get('password')
#
# if not email or not password:
# raise serializers.ValidationError("Email과 Password는 필수 입력 항목입니다")
#
# user = authenticate(email=email, password=password)
#
# if not user:
# raise serializers.ValidationError("올바른 email과 password를 입력하세요.")
#
# attrs['user'] = user
#
# return attrs
# authenticate() : 사용자의 자격 증명을 확인하고, 유효한 경우 사용자 객체를 반환하는 함수,
# 인증이 성공하면 해당 사용자 객체(User)를 반환
# 인증이 실패하면 None을 반환
# 📌 self.context란?
# Django REST Framework(DRF)에서 Serializer는 뷰(View)에서 전달된 context를 가질 수 있음.
# self.context.get('request')로 요청 객체를 가져올 수 있음.
# serializer = UserLoginSerializer(data=request.data, context={"request": request})
# ✔ 즉, 기본적으로 attrs에는 email과 password가 있지만,
# ✔ request는 현재 요청을 보낸 사용자에 대한 추가적인 정보를 포함하는 역할을 함.
# Django 기본 인증 방식 중 하나인 **SessionAuthentication**을 사용할 경우, 로그인한 사용자의 세션을 확인할 수 있음.
# request를 전달하면, Django가 현재 세션에서 로그인된 사용자 정보를 확인하는 데 활용할 수 있음.
# ✔ authenticate(request, username, password)를 사용하면, Django가 세션을 유지하며 로그인할 수 있도록 지원
class UserLoginSerializer(serializers.Serializer):
email = serializers.CharField(required=True)
password = serializers.CharField(required=True, write_only=True)
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
if email and password:
user = authenticate(request=self.context.get('request'), **attrs)
if not user:
raise serializers.ValidationError(
# 오류 메세지 표준화
# detail 오류 메시지 전달
# code 오류의 종류 지정
detail='Unable to log in with provided credentials.', code='authorization'
)
else:
raise serializers.ValidationError(
detail='Must be Required "email" and "password".', code='authorization'
)
attrs['user'] = user
return attrs
ModelSerializer
를 상속받아 사용ModelSerializer
를 상속받아 사용class ReviewDetailSerializer(serializers.ModelSerializer):
user = UserDetailSerializer(read_only=True)
restaurant = RestaurantSerializer(read_only=True)
# ✔ user 필드는 UserDetailSerializer를 사용하여 직렬화
# ✔ 즉, Review 모델의 user 필드가 단순한 id가 아니라 사용자 정보 전체를 포함하도록 변환됨.
# ✔ read_only=True → POST 요청 시 클라이언트가 값을 보내지 않아도 됨 (perform_create()에서 자동 설정됨).
# 직렬화 시키지 않으면 Django REST Framework는 User와 Restaurant 객체를 자동으로 JSON으로 변환할 수 없음
# 따라서, 단순히 변수 이름을 바꿔도 해결되지 않음
class Meta:
model = Review
fields = "__all__"
READ_ONLY_FIELDS = ['pk','restaurant']
class ReviewSerializer(serializers.ModelSerializer):
# 리뷰 제목, 텍스트와 함께 작성자의 정보, 레스토랑의 정보를 함께 보냄
user = UserDetailSerializer(read_only=True)
class Meta:
model = Review
fields = "__all__"
# user, restaurant 필드는 serializer.save()의 인자로 전달할 것이기 때문에 읽기 전용 필드로 설정
read_only_fields = ["id", "restaurant"]
rest_framework.generics
의 CreateAPIView
를 상속받아 구현rest_framework.views
의 APIView
를 상속받아 구현rest_framework.generics
의 RetrieveUpdateDestroyAPIView
를 상속받아 구현from django.contrib.auth import login, get_user_model
from django.shortcuts import render
from rest_framework import status
from rest_framework.generics import CreateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from users.serializers import UserSerializer, UserLoginSerializer, UserDetailSerializer
class UserCreatAPIView(CreateAPIView):
serializer_class = UserSerializer
permission_classes = [AllowAny]
class UserLoginAPIView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = UserLoginSerializer(data=request.data)
if serializer.is_valid():
login(request, serializer.validated_data.get('user'))
return Response({'message': 'login success'}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserDetailView(RetrieveUpdateDestroyAPIView):
queryset = get_user_model().objects.all()
serializer_class = UserDetailSerializer
# 로그인 처리
def get_object(self):
return get_object_or_404(get_user_model(), pk = self.request.user.pk)
rest_framework.generics
의 ListCreateView
를 상속받아 구현rest_framework.generics
의 RetriveUpdateDestroyView
를 상속받아 구현from rest_framework import serializers
from restaurants.serializers import RestaurantSerializer
from reviews.models import Review
from users.serializers import UserSerializer, UserDetailSerializer
from django.shortcuts import render
from rest_framework.generics import ListCreateAPIView, get_object_or_404, RetrieveUpdateDestroyAPIView
from rest_framework.permissions import IsAuthenticated
from restaurants.models import Restaurant
from reviews.models import Review
from reviews.serializers import ReviewSerializer
# Create your views here.
class ReviewListCreateView(ListCreateAPIView):
queryset = Review.objects.all().order_by('-created_at')
serializer_class = ReviewSerializer
def get_queryset(self):
# Django ORM에서는 외래 키(ForeignKey)를 필터링할 때 _id를 사용해야 함 (restaurant_id)
return self.queryset.filter(restaurant_id=self.kwargs.get('restaurant_pk'))
def perform_create(self, serializer):
restaurant_id = self.kwargs.get('restaurant_pk')
restaurant = get_object_or_404(Restaurant, pk = restaurant_id)
serializer.save(user=self.request.user, restaurant=restaurant)
class ReviewDetailView(RetrieveUpdateDestroyAPIView):
serializer_class = ReviewDetailSerializer
permission_classes = [IsAuthenticated]
def get_object(self):
return get_object_or_404(Review, pk=self.kwargs.get('review_pk'), user=self.request.user) # ✅ 개별 객체 가져오기
이번 프로젝트에서는 URL Include 방식을 사용하여 url 엔드포인트들을 구성하겠습니다.
아래의 요구사항을 충족하는 앱의 urls.py 생성하고 config / urls.py 에서 include 를 사용해주세요.
login/
signup/
logout/
django.contrib.auth.views
의 LogoutView
를 매핑해줍니다.profile/<int:pk>/
from django.urls import path
from users import views
urlpatterns =[
path('signup/', views.UserCreatAPIView.as_view(), name='user-signup'),
path('login/', views.UserLoginAPIView.as_view(), name='user-login'),
path('detail/<int:pk>', views.UserDetailView.as_view(), name='user-detail'),
]
restaurants/<int:restaurant_id>/reviews
reviews/<int:review_id>
from django.urls import path
from reviews import views
urlpatterns = [
path('restaurants/<int:restaurant_pk>/review/', views.ReviewListCreateView.as_view(),
name='review-list-create'),
path('review/<int:review_pk>/', views.ReviewDetailView.as_view(),
name='review-detail'),
]
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('restaurants.urls')),
path('', include('users.urls')),
path('',include('reviews.urls')),
]
django-restframework
에 대해서 이해하고 적절히 활용할 수 있다.TDD(Test Driven Development)
방식에 대해서 이해하고 수행할 수 있다.Django Test
에 대해서 이해하고 테스트 코드를 적절히 작성할 수 있다.rest_framework.views
의API View
에 대해서 이해하고 적절히 활용할 수 있다.rest_framework_simplejwt
에 대해서 이해하고 활용할 수 있다.drf-yasg
에 대해서 이해하고 활용하여Swagger
문서를 만들 수 있다.- 로컬환경과 배포환경을 각각 적절하게 설정할 수 있다.
PythonAnywhere
를 활용하여 배포하는 과정을 이해하고 배포할 수 있다.
지난 과제에서는 TDD 방식과 APIView, GenericView를 이용하여 User, Review 모델에 대한 url 을 구성해보았습니다.
이번 과제에서는 로컬, 배포환경의 설정을 구분해주고 TDD 방식과djangorestframework_simplejwt
를 이용하여 JWT 로그인, 로그아웃 기능을 간단하게 구현해보고PythonAnywhere
을 활용해서 배포까지 진행해보겠습니다.
아래의 요구사항을 참고하여 코드를 구성하세요.
DEBUG
ALLOW_HOSTS
STATIC_URL
STATIC_DIRS
STATIC_ROOT
MEDIA_URL
MEDIA_ROOT
DATABASES
# prod.py 예시
from .base import *
DEBUG = False
ALLOWED_HOSTS = ['PythonAnywhere 배포도메인']
# Static
STATIC_URL = 'static/'
STATIC_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / '.static_root'
# Media
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# DB
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# local.py 예시
from .base import *
DEBUG = True
ALLOWED_HOSTS = []
# Static
STATIC_URL = 'static/'
STATIC_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / '.static_root'
# Media
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': SECRET['DB']['NAME'], # 데이터베이스 이름
'USER': SECRET['DB']['USER'], # 사용자 이름
'PASSWORD': SECRET['DB']['PASSWORD'], # 비밀번호
'HOST': SECRET['DB']['HOST'], # 데이터베이스 서버 주소
'PORT': SECRET['DB']['PORT'], # MySQL의 기본 포트
}
}
ln -sf 대상파일 심볼릭링크파일
명령어를 사용하여 심볼릭 링크를 생성해주세요.# 예시
# pwd가 프로젝트 루트 디렉터리 / config / settings 라고 가정
ln -sf prod.py settings.py
.gitignore
에 심볼릭 링크 파일을 추가하여 github에 추가되지 않도록 해주세요.# manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
BASE_DIR = Path(__file__).resolve().parent.parent.parent
djangorestframework-simplejwt
라이브러리 설치JWTAuthentication
추가하기class UserTokenLoginAPIView(TokenObtainPairView):
permission_classes = [AllowAny]
class UserTokenLogoutAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
try :
refresh_token = request.data['refresh']
token = RefreshToken(refresh_token)
token.blacklist() # 로그아웃시 못쓰게하려고
return Response({"message": "Logout Successfully"}, status= status.HTTP_205_RESET_CONTENT)
except Exception as e:
return Response({"error": "Invalid Token"}, status= status.HTTP_400_BAD_REQUEST)
# users/urls.py
path('login/', views.UserTokenLoginAPIView.as_view(), name='user-login'),
path('logout/', views.UserTokenLogoutAPIView.as_view(), name='user-logout'),
poetry export --without-hashes --format=requirements.txt > requirements.txt
console 에 접속하고, git clone
명령어를 사용하여 원격 레포지토리를 클론받습니다.
특정 브랜치를 클론 받으려면 아래의 명령어를 사용하세요.
```markdown
git clone --branch <branch_name> --single-branch <repository_url>
```
cd
명령어로 프로젝트 루트 디렉터리 / config / settings
로 이동하고, 다음 명령어를 사용하여 settings.py 심볼릭 링크 파일을 생성합니다.
ln -sf prod.py settings.py
디렉터리에 requirements.txt 파일을 실행
pip install -r requirement.txt
static 파일 처리를 위해 STATIC_ROOT = '.static_root' 깃에 다시 push 한후 git pull
python manage.py collectstatic 스태틱 파일 한곳으로 모는 명령어, ls -al 을 통해 확인(.static_root 생성)
Source Code 부분 변경
WSGI 부분 변경
에러는 error log 로 확인
static 경로 넣기
1. 밑의 static files 부분
2. url /static/
3. directory -> 복사한 경로/.static_root