우선은 오늘 어제 생겼던 permission 인증 오류가 일어난 부분에 대해서 잘못 작성한 부분이 있는지 코드를 살펴보았다.
# drf_project/permissions.py
class IsAdminOrIsAuthenticatedReadOnly(BasePermission):
# admin은 읽기 쓰기 가능 / 일반 사용자는 읽기만 가능
SAFE_METHODS = ('GET', ) # 일반 사용자 권한 허용 : GET
message = '접근 권한이 없습니다.'
def has_permission(self, request, view):
user = request.user
if not user.is_authenticated:
response = {
"detail": "서비스를 이용하기 위해서는 로그인이 필요합니다.",
}
raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)
# 인증된 사용자가 관리자(admin)일 경우
if user.is_authenticated and user.is_admin:
return True
# 사용자가 인증되었고, 권한이 GET일 경우(65줄)
elif user.is_authenticated and request.method in self.SAFE_METHODS:
return True
return False
해당 코드는 permissions.py에서 작성한 Custom Permission Class이다.
해당 커스텀 permission을 가지고 로그인 관련 API를 설정하려고 했으나 오류가 발생했던 것이다.
왜 오류가 발생했는가 싶어서 user/views.py를 확인하니 다음과 같이 코드가 구현되어 있었다.
# user/views.py
class UserView(APIView):
permission_classes = [IsAdminOrIsAuthenticatedReadOnly] # 사용자 지정 permission
# 로그인 한 사용자 보여주기
def get(self, request):
user = request.user # 로그인한 유저 정보
if not user.is_authenticated:
return Response({"fail": "로그인이 필요합니다."}, status=status.HTTP_401_UNAUTHORIZED)
return Response(UserSerializer(request.user).data, status=status.HTTP_200_OK)
# 로그인 기능
@csrf_exempt
def post(self, request):
username = request.data.get('username', '')
password = request.data.get('password', '')
user = authenticate(request, username=username, password=password)
if not user:
return Response({"fail": "계정이 존재하지 않거나 패스워드가 일치하지 않습니다."}, status=status.HTTP_401_UNAUTHORIZED)
login(request, user)
return Response({"success": "로그인 완료"}, status=status.HTTP_200_OK)
# 로그아웃
def delete(self, request):
logout(request)
return Response({"success": "로그아웃 완료"}, status=status.HTTP_200_OK)
만들어준 permission은 로그인을 한 상태이거나 관리자(admin)인 경우에만 접근이 가능하도록 설정이 되어있었기 때문에 해당 기능이 잘 작동되지 않았던 것이다.
그래서 GET 요청과 POST, DELETE 요청이 있는 부분을 다음과 같이 분리하여 다른 클래스로 지정해주었다.
# user/views.py
class UserView(APIView):
permission_classes = [IsAdminOrIsAuthenticatedReadOnly] # 사용자 지정 permission
# 로그인 한 사용자 보여주기
def get(self, request):
user = request.user # 로그인한 유저 정보
if not user.is_authenticated:
return Response({"fail": "로그인이 필요합니다."}, status=status.HTTP_401_UNAUTHORIZED)
return Response(UserSerializer(request.user).data, status=status.HTTP_200_OK)
class UserSignView(APIView):
permission_classes = [permissions.AllowAny] # 누구나 view 조회 가능
# 로그인 기능
@csrf_exempt
def post(self, request):
username = request.data.get('username', '')
password = request.data.get('password', '')
user = authenticate(request, username=username, password=password)
if not user:
return Response({"fail": "계정이 존재하지 않거나 패스워드가 일치하지 않습니다."}, status=status.HTTP_401_UNAUTHORIZED)
login(request, user)
return Response({"success": "로그인 완료"}, status=status.HTTP_200_OK)
# 로그아웃
def delete(self, request):
logout(request)
return Response({"success": "로그아웃 완료"}, status=status.HTTP_200_OK)
다음 부분을 분리하고 user/urls.py에 UserSignView 클래스를 연결하는 URL을 설정해주고 Postman에서 확인을 해보았다.


sign/ 링크로 해당 값을 보내면 로그인과 로그아웃 API가 잘 넘어오는 것을 볼 수 있다.
그리고 로그인된 사용자에 대한 정보 값을 확인해도 잘 출력되는 것을 볼 수 있었다.

다음으로 관리자(admin) 계정으로 django admin 페이지에 접속할 때 나오는 웹 페이지에 대한 설정을 해주었다.
user에 관한 설정을 해줄 것이기 때문에 user/admin.py 파일을 살펴보았다.
원래는 다음과 같이 짧게 코드가 구성되어 있었는데, 해당 부분을 살짝 바꿔보고자 한다.
# user/admin.py
from django.contrib import admin
from user.models import User, UserProfile, Hobby
# Register your models here.
admin.site.register(User)
admin.site.register(UserProfile)
admin.site.register(Hobby)
원래는 user/models.py에 정의해준 모델들이 표시될 수 있도록 만들어 주었다.
하지만 여기서 코드를 추가하여 살짝 바꿔보고자 한다.
# user/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from user.models import User, UserProfile, Hobby
# class UserAdmin(admin.ModelAdmin) 직접 설정하려면..
# Inline은 역참조 시 사용이 가능하다.
# Inline 방식은 두가지(TabulaInline(가로) / StackInline(세로))
class UserProfileInline(admin.StackedInline):
model = UserProfile
# Django Document에서 정의해준 user admin 코드
class UserAdmin(BaseUserAdmin):
list_display = ('id', 'username', 'fullname', 'email')
list_display_links = ('username', )
list_filter = ('username', )
search_fields = ('username', 'email', )
fieldsets = (
("info", {'fields': ('username', 'password', 'email', 'fullname', 'join_date',)}),
('Permissions', {'fields': ('is_admin', 'is_active', )}),)
filter_horizontal = []
inlines = (
UserProfileInline,
)
def get_readonly_fields(self, request, obj=None):
if obj:
return ('username', 'join_date', )
else:
return ('join_date', )
# Register your models here.
admin.site.register(User, UserAdmin)
admin.site.register(UserProfile)
admin.site.register(Hobby)
UserAdmin 클래스를 생성해주고 Django Document에서 지정되어 있는 관리자 모델을 import 해주었다.
하지만 이상태로 사용하기엔 만든 클래스 UserAdmin과 Django에서 지정해준 UserAdmin의 이름이 중복되기 때문에 import를 해줄 때 as BaseUserAdmin으로 이름을 바꿔서 사용해주었다.
그리고 User 모델에서 설정해준 필드들을 띄워주기 위해 띄워주고자 하는 값들을 넣어주고, User 생성 시 어떤 값들이 들어갈 것인지도 적어주었다.
마지막으로 UserProfileInline 클래스를 만들어서 User 생성 시 해당 유저에 대한 UserProfile도 한 페이지 내에서 설정이 가능하도록 설정해주었다.
여기서는 StackedInline을 사용하여 세로로 UserProfile 설정 칸이 나오도록 만들어 주었다.
이후 장고 프로젝트를 실행하고 admin 페이지로 이동하여 확인해보도록 했다.


마지막으로 로그인된 유저의 article을 조회하고 해당 유저가 article을 작성하는 API를 만들어 주었다.
해당 API를 만들기 전에 다음과 같은 조건을 넣어서 만들어보고자 하였다.
1. 게시글 작성 시 title, category, content 조건 부여하기
2. Article 모델에 노출 시작 일자(show_date_start)와 노출 종료 일자(show_date_end) 추가하기
3. 노출 시작 일자와 노출 종료 일자 사이에 있는 게시글을 보여주는 조건 부여하기
4. 관리자(admin) 또는 가입 후 7일이 지난 사용자만 게시글 생성이 가능하도록 조건 부여하기
우선 1번 조건인 title, category, content에 대한 조건을 부여해줬다.
해당 조건은 게시글 작성에 대한 조건이기 때문에 ArticleView 안에 있는 post 함수 내에 다음과 같이 조건을 넣어주었다.
# blog/views.py
class ArticleView(APIView):
@csrf_exempt
def post(self, request):
title = request.data.get("title", "")
contents = request.data.get("contents", "")
categories = request.data.get("categories", []) # 카테고리는 여러개라 리스트로
# 조건1. 만약 title이 5자 이하라면 게시글을 작성할 수 없다고 리턴해주세요.
if (len(title) <= 5):
return Response({"error": "제목은 5글자 이상 작성해주세요."}, status=status.HTTP_400_BAD_REQUEST)
# 조건2. 만약 content가 20자 이하라면 게시글을 작성할 수 없다고 리턴해주세요.
if (len(contents) <= 20):
return Response({"error": "내용은 20글자 이상 작성해주세요."}, status=status.HTTP_400_BAD_REQUEST)
# 조건3. 만약 카테고리가 지정되지 않았다면 카테고리를 지정해야 한다고 리턴해주세요.
if not categories:
return Response({"error": "카테고리가 있어야 합니다."}, status=status.HTTP_400_BAD_REQUEST)
조건1은 title에 대한 조건으로, 입력받은 title의 길이가 5글자 이하라면 HTTP Status 400을 반환하도록 해주었다.
조건2는 content에 대한 조건으로, 입력받은 content의 길이가 20글자 이하라면 HTTP Status 400을 반환하도록 해주었다.
조건3은 category에 대한 조건으로, 입력받은 category가 존재하지 않는다면 HTTP Status 400을 반환하도록 해주었다.
이것으로 게시글 생성 시 들어가는 title, content, category에 대한 조건을 설정해주었다.
이제 다음으로 blog/models.py의 Article 모델 안에 노출 시작 일자와 노출 종료 일자를 추가해주었다.
# blog/models.py
# 아티클(게시글) 모델
class Article(models.Model):
user = models.ForeignKey('user.User', verbose_name="사용자", on_delete=models.CASCADE)
title = models.CharField("제목", max_length=100)
category = models.ManyToManyField(to=Category, verbose_name="카테고리")
content = models.TextField("내용")
show_date_start = models.DateTimeField("노출 시작 일자", auto_now_add=True)
show_date_end = models.DateTimeField("노출 종료 일자", default=timezone.now()+timedelta(days=7))
def __str__(self):
return f'{self.user.username}님의 글입니다. : {self.title}'
주어진 조건이 생성 일자만 적어주면 되는 것이기에 DateField를 사용해도 상관 없다.
하지만 여기서는 시간대도 함께 생성하도록 DateTimeField를 사용하여 해당 필드를 설정해주었다.
노출 시작 일자인 show_date_start 필드에는 게시글이 생성되는 시간을 넣어주었고, 노출 종료 일자인 show_date_end 필드에는 설정하지 않는 이상 default 값으로 현재 시간에서 7일을 더한 값을 넣어주었다.
필드들이 정상적으로 생성이 되었다면 해당 게시글을 보여주기 위해 ArticleView 안에 있는 get 함수에 다음 조건을 넣어주었다.
# blog/views.py
class ArticleView(APIView):
def get(self, request):
user = request.user # 로그인한 사용자를 user 변수에 담는다.
# get, filter, exclude 사용 시 Field Lookups 문법
# __contains : 특정 string이 포함된 object 찾기
# __startswith / __endswith : 특정 string으로 시작 / 끝나는 object 찾기
# __gt / __lt / __gte / __lte : 큼(>) / 작(<) / 큼같(>=) / 작같(<=)
# __in : 특정 list에 포함된 object 찾기
# Q : 쿼리에서 and(&) 및 or(|) 사용
show_date_now = timezone.now()
terms = Q(user=user) & Q(show_date_start__lte=show_date_now) & Q(show_date_end__gte=show_date_now)
# order_by : queryset 정렬
# "이름" : 오름차순 / "-이름" : 내림차순 / "?" : 랜덤
articles = Article.objects.filter(terms).order_by("show_date_start")
return Response(ArticleSerializer(articles, many=True).data)
우선 게시글을 보고자 하는 현재 시간을 show_date_now 변수에 넣어주었다.
그리고 Q를 사용하여 다음과 같은 조건을 지정해주고 terms 변수 안에 넣어주었다.
조건문(if문)과 비슷한 형태인 Q는 Query에서 and(&) 및 or(|) 조건을 이용하고자 사용하는 것이고, 해당 Q 안에 __lte와 __gte를 사용하여 노출 시작 일자와 노출 종료 일자 사이에 있는 값을 찾아내는 조건을 넣어주었다.
해당 조건에 부합하는 값을 Article.objects.filter(terms)를 이용하여 Article 테이블에서 조건이 일치하는 값들만 골라서 articles 변수에 넣어주었다.
그리고 해당 값들은 .order_by("show_date_start")를 통해 현재 시간에 따른 오름차순으로 정렬되도록 설정해주었다.
다음은 Q를 이용하여 코드를 구현하다가 공부하게 된 명령어들에 대한 정리이다.
get, filter, exclude를 사용할 때 같이 쓸 수 있는 Field Lookups 문법
- __contains : 특정 string이 포함된 object 찾기
- __startswith / __endswith : 특정 string으로 시작 / 끝나는 object 찾기
- __gt / __lt / __gte / __lte : 큼(>) / 작(<) / 큼같(>=) / 작같(<=)
- __in : 특정 list에 포함된 object 찾기
kwargs 활용
해당 request.data를 다음과 같이 간략하게 줄일 수 있다.
{"user": "user1", "passowrd": "1234, "name": "유저1"}
→ 모델명.objects.create(**request.data)
정렬(.order_by())
- "이름" : 오름차순
- "-이름" : 내림차순
- "?" : 랜덤
쿼리 조건문(Q)
- and [ & ]
- or [ | ]
이제 마지막으로 관리자(admin) 또는 가입 후 7일이 지나야 게시글 생성이 가능하도록 조건을 부여해보자.
해당 조건은 drf_project/permissions.py 파일 내에 다음과 같은 조건을 지정해주고 해당 permission을 사용해주도록 하였다.
# drf_project/permissions.py
# 1. 기존 게시글(article) 생성 기능 유지
# 2. admin 유저 또는 가입 후 7일이 지난 사용자만 게시글 생성 가능
# 3. 로그인한 유저만 게시글 조회 가능
class IsAdminOrMoreThanOneWeekUser(BasePermission):
SAFE_METHODS = ('GET', ) # 일반 사용자 권한 허용 : GET
message = '접근 권한이 없습니다.'
def has_permission(self, request, view):
user = request.user
if not user.is_authenticated:
response = {
"detail": "서비스를 이용하기 위해서는 로그인이 필요합니다.",
}
raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)
# 인증된 사용자가 관리자(admin)일 경우
if user.is_authenticated and user.is_admin:
return True
# 사용자가 인증되었고, 가입 후 7일이 지난 사용자일 경우
if user.is_authenticated and request.user.join_date < (timezone.now() - timedelta(days=7)):
return True
# 사용자가 인증되었고, 권한이 GET일 경우
if user.is_authenticated and request.method in self.SAFE_METHODS:
return True
return False
이렇게 지정된 Custom Permission을 ArticleView에 적용해주었다.
# blog/views.py
class ArticleView(APIView):
# permission_classes = [permissions.IsAuthenticated] # 로그인 된 사용자만 view 조회 가능
parmission_classes = [IsAdminOrMoreThanOneWeekUser] # 사용자 지정 permission (4일차)
# 게시글 조회, 로그인한 사용자만 가능
def get(self, request):
user = request.user # 로그인한 사용자를 user 변수에 담는다.
if not user.is_authenticated:
return Response({"message": "로그인이 필요합니다."}, status=status.HTTP_401_UNAUTHORIZED)
show_date_now = timezone.now()
terms = Q(user=user) & Q(show_date_start__lte=show_date_now) & Q(show_date_end__gte=show_date_now)
articles = Article.objects.filter(terms).order_by("show_date_start")
return Response(ArticleSerializer(articles, many=True).data)
# 게시글 작성
@csrf_exempt
def post(self, request):
user = request.user # 로그인한 사용자
# 제목, 내용, 카테고리 작성
title = request.data.get("title", "")
contents = request.data.get("contents", "")
categories = request.data.get("categories", []) # 카테고리는 여러개라 리스트로
if (len(title) <= 5):
return Response({"error": "제목은 5글자 이상 작성해주세요."}, status=status.HTTP_400_BAD_REQUEST)
if (len(contents) <= 20):
return Response({"error": "내용은 20글자 이상 작성해주세요."}, status=status.HTTP_400_BAD_REQUEST)
if not categories:
return Response({"error": "카테고리가 있어야 합니다."}, status=status.HTTP_400_BAD_REQUEST)
# article = Article.objects.create(user=user, title=title, content=content)
article = Article(
user=user,
title=title,
content=contents,
)
category_list = [Category.objects.get(name=category) for category in categories]
article.save()
article.category.set(category_list)
return Response({"message": "게시글 작성 완료"}, status=status.HTTP_200_OK)
이제 마지막으로 Postman을 통해 해당 API가 정상적으로 작동하면서 DB에도 데이터 값이 잘 들어가는지 확인해보았다.


Github : https://github.com/J1NU2/DRF_Practice
GitHub - J1NU2/DRF_Practice
Contribute to J1NU2/DRF_Practice development by creating an account on GitHub.
github.com
-
오늘은 로그인 및 로그아웃 코드를 고치고, 관리자 페이지에 대한 설정과 게시글에 대한 조건을 넣어주면서 코드를 구현해보았다.
혼자서 계속 리마인드해보면서 이해가 되지 않는 부분은 현재 팀의 팀장님께서 도움을 통해 많은 도움을 받아서 공부를 진행했다.
도움을 받아서 코드 구현에 있어 이해를 해가면서 정리를 하니 조금 더 재미있게 공부를 할 수 있었다.
:D
'TIL 및 WIL > TIL (Today I Learned)' 카테고리의 다른 글
| [TIL] 2022.06.23 (Django 심화, DRF 6) (0) | 2022.06.23 |
|---|---|
| [TIL] 2022.06.22 (Django 심화, DRF 5) (0) | 2022.06.23 |
| [TIL] 2022.06.20 (Django 심화, DRF 3) (0) | 2022.06.21 |
| [TIL] 2022.06.16 (Django 심화, DRF 2) (0) | 2022.06.16 |
| [TIL] 2022.06.15 (Django 심화, DRF 1) (0) | 2022.06.15 |