모델에 User 항목 추가하기
User는 기본적으로 contrib.auth를 통해 관리된다. 해당 라이브러리 안의 Users라는 모델을 불러와 User가 어떤 형식으로 구성되어 있는지 shell을 통해 알아보자.
>>> from django.contrib.auth.models import User
>>> User
<class 'django.contrib.auth.models.User'>
>>> User._meta.get_fields()
(<ManyToOneRel: admin.logentry>, <django.db.models.fields.AutoField: id>, <django.db.models.fields.CharField: password>, <django.db.models.fields.DateTimeField: last_login>, <django.db.models.fields.BooleanField: is_superuser>, <django.db.models.fields.CharField: username>, <django.db.models.fields.CharField: first_name>, <django.db.models.fields.CharField: last_name>, <django.db.models.fields.EmailField: email>, <django.db.models.fields.BooleanField: is_staff>, <django.db.models.fields.BooleanField: is_active>, <django.db.models.fields.DateTimeField: date_joined>, <django.db.models.fields.related.ManyToManyField: groups>, <django.db.models.fields.related.ManyToManyField: user_permissions>)
User가 가지고 있는 필드는 매우 많다. id와 password부터 슈퍼유저 여부, 이메일 등등 다양한 필드를 가지고 있다.
현재 User의 리스트를 먼저 살펴보자.
>>> User.objects.all()
<QuerySet [<User: admin>]>
현재는 이전에 만들어 두었던 관리자 계정인 admin만 남아있는 상태다.
이 유저(admin)를 Question 모델에서도 사용할 수 있게 하려면, 해당 모델에 user의 정보를 알려주어야 한다.
# mysite/polls/models.py
class Question(models.Model):
question_text = models.CharField(max_length=200, verbose_name='질문')
pub_date = models.DateTimeField(auto_now_add=True, verbose_name='생성일')
# 해당 질문 레코드 작성자, User에서 FK로 가져온 값임 1(user):N(Question)의 관계
owner = models.ForeignKey('auth.User', related_name='questions', on_delete=models.CASCADE, null=True)
owner 정보를 모델에 추가해주었다. key의 이름은 auth.User이며, on_delete=models.CASCADE 옵션을 넣어주어 해당 계정이 삭제될 경우 관련된 모든 질문 레코드도 삭제하라는 의미이다.
이후 모델이 변경되었으니 마이그레이션을 진행한다.
❯ python manage.py makemigrations
Migrations for 'polls':
polls/migrations/0002_question_owner_alter_question_pub_date_and_more.py
- Add field owner to question
- Alter field pub_date on question
- Alter field question_text on question
❯ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
Applying polls.0002_question_owner_alter_question_pub_date_and_more... OK
이후 shell을 통해 user와 question 모델이 어떻게 연결되었는지 살펴보자.
# 라이브러리 임포트
>>> from django.contrib.auth.models import User
>>> from polls.models import *
# User 중 첫번째 선택
>>> user = User.objects.first()
# Question 모델에서 related_name으로 사용한 것을 메서드로 사용
>>> user.questions.all()
<QuerySet []>
# 유저에 연결된 질문을 찾는 쿼리 출력
>>> print(user.questions.all().query)
SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date", "polls_question"."owner_id" FROM "polls_question" WHERE "polls_question"."owner_id" = 1
User 중 하나를 불러와 그에 연결된 질문을 찾을 때는 위 models.py에서 작성했던 realated_name을 사용하면 알 수 있다. 위와 같이 realted_name 파라미터를 지정해준다면, shell에서 해당 이름으로 FK를 연결해 찾을 수 있다. (지정해주지 않을 경우 모델 이름 + _set으로 찾음 (ex)Question.choice_set.all() )
해당 레코드를 찾는 쿼리를 살펴보면, 유저의 id를 이용해 조건에 맞는 레코드를 찾는다는 것을 알 수 있다. 이는 마치 Question에 연결된 choice를 불러올 때와 비슷한 모습을 보인다.
# user 정보로 question 목록 불러올 때
>>> print(user.questions.all().query)
SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date", "polls_question"."owner_id" FROM "polls_question" WHERE "polls_question"."owner_id" = 1
# question 정보로 choice 목록 불러올 때
>>> print(q.choice_set.all().query)
SELECT "polls_choice"."id", "polls_choice"."question_id", "polls_choice"."choice_text", "polls_choice"."votes" FROM "polls_choice" WHERE "polls_choice"."question_id" = 1
User 관리 기능 구현
User 모델을 Serializer로 만들고 view로 구현하기
serializer.py에 User 모델을 위한 시리얼라이저를 새롭게 추가한다.
class UserSerializer(serializers.ModelSerializer):
questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'questions']
여기서 PrimaryKeyRelatedField는 해당 모델의 PK를 통해서 연결된 항목이 있을 경우 사용한다. 1:N의 관계일 경우 many라는 파라미터를 True로 전달한다. 위 예시에서는 1명의 User에 대해서 N개의 Question을 가질 수 있는 관계이므로 위와 같이 코드를 작성한다.
위와 같이 명시되어야 하는 이유는 해당 정보가 User 모델 내 있는 정보가 아닌, Question 모델에 있기 때문이다. 즉, 시리얼라이저 입장에서 다른 모델의 테이블을 살펴봐야 하므로 위와 같이 명시해줘야 한다.
이제 view를 구현해보자. 이전에 사용했던 generics의 ListCreateAPIView와 RetrieveUpdateDestroyAPIView를 이용해 GET,POST,PUT,DELETE가 가능한 view를 생성한다.
# mysite/polls_api/views.py
from rest_framework import status, mixins, generics
from polls.models import Question
from .serializers import QuestionSerializer, UserSerializer
from django.contrib.auth.models import User
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
...
view를 추가했다면 당연히 URL도 추가해주어야 한다.
from django.urls import path
from . import views
app_name = 'polls_api'
urlpatterns = [
path('question/', views.QuestionList.as_view(), name='question_list'),
path('question/<int:pk>/', views.QuestionDetail.as_view(), name='question_detail'),
# user 관련 URL 추가
path('users/', views.UserList.as_view(), name='user_list'),
path('users/<int:pk>/', views.UserDetail.as_view(), name='user_detail'),
]
이제 해당 URL로 접속해보자.
User List라는 이름으로 현재 등록된 User 목록이 노출되는 것을 볼 수 있다. 현재 User는 Admin 1개이자, 가지고 있는 question이 없는 상태이다.
일반 User 생성하기
위 Admin 계정은 Django 설정 시 만들었던 슈퍼계정(관리자)이기 때문에, 각각의 글을 쓸 수 있는 일반 User를 생성해보려고 한다.
1. Form을 사용해 일반 User 생성하기 (django 제공 기능 활용)
일반적인 회원가입 페이지처럼 User에게 정보를 폼(form)으로 제공받아보자. polls 앱의 views.py를 열어 다음과 같은 클래스를 추가하자.
# mysite/polls/views.py
from django.views import generic # rest framework에서 사용한 generics와 비슷한 역할
from django.urls import reverse_lazy
from django.contrib.auth.forms import UserCreationForm # django 기본 제공 회원가입 폼
...
class SignUpView(generic.CreateView):
form_class = UserCreationForm
# reverse_lazy: urls.py에서 정의한 이름을 기반으로 URL을 동적으로 생성
# polls_api의 user_list로 해당 정보를 보내줌
success_url = reverse_lazy('user-list')
template_name = 'polls/signup.html'
Django에서 기본으로 제공하는 회원가입 폼과 generic의 CreateView를 상속받는다. 입력된 정보는 polls_api의 user_list로 전달되어 POST된다. 템플릿으로는 signup.html을 사용하는데, 아래와 같이 구현한다.
<!-- mysite/polls/templates/polls/signup.html -->
<h2>회원가입</h2>
<form method="post">
<!-- csrf_token: 보안을 위해 필요 -->
{% csrf_token %}
{{ form.as_p }}
<button type="submit">회원가입</button>
</form>
해당 화면을 보여줄 url도 등록해주어야 한다.
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('<int:question_id>/vote/', views.vote, name='vote'),
path('<int:question_id>/results/', views.results, name='results'),
# 회원가입 페이지 URL 추가
path('signup/', views.SignUpView.as_view(), name='signup'),
]
이후 해당 URL('polls/signup')에 접속하면 아래와 같은 화면이 노출된다.
ID, PW, PW 확인란이 기본으로 제공되며 기본적인 규칙 또한 설정되어 있는 모습을 볼 수 있다. 해당 정보를 입력해 회원가입을 완료하고 나면 User가 추가되었는지 알 수 있도록 User list 화면으로 이동하는 것을 볼 수 있다.
2. Serializer를 사용하여 User 생성하기
시리얼라이저를 이용하면 REST API를 통해서도 User를 생성할 수 있다. 일단 User를 생성하는 역할을 하는 시리얼라이저를 생성해보자.
# mysite/polls_api/serializer.py
# User 모델을 등록하는 Serializer
class RegisterSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'password']
# write_only: 비밀번호 필드를 읽기 전용으로 설정
extra_kwargs = {'password': {'write_only': True}}
위와 같이 시리얼라이저를 만들어준다. 특히 password 필드의 경우 읽지 못하도록 가리는 옵션인 'write_only'를 True로 전달해준다.
그 다음 views.py로 이동해 이 시리얼라이저를 이용할 수 있도록 설정한다.
# mysite/polls_api/views.py
from .serializers import QuestionSerializer, UserSerializer, RegisterSerializer
class RegisterView(generics.CreateAPIView):
queryset = User.objects.all()
serializer_class = RegisterSerializer
CreateAPIView를 사용해 POST 메서드를 가진 View를 생성해준다.
마지막으로 urls.py에서 해당 view가 작동될 URL을 등록한다.
from django.urls import path, include
from .views import *
urlpatterns = [
...
path('register/', RegisterView.as_view(), name='register'),
...
]
이름을 register로 갖는 URL을 하나 추가했다.
이제 서버를 열어 해당 URL로 진입해보자.
등록화면이 정상적으로 노출되는 것을 알 수 있다. User list가 노출되는 부분에 "Method \"GET\" not allowed"라는 메시지가 출력되는데, 이는 위에서 view를 CreateAPIView로 구성했기 때문이다. GET까지 사용하려면 ListCreateAPIView를 사용하면 된다.
이제 Username과 Password를 입력해 등록이 잘 되는지 살펴보자.
정상적으로 잘 구현된다. 만약, username이 같은 User를 한번 더 생성하려고 하면 어떻게될까?
400 에러코드가 반환되며 이미 해당 username을 사용하는 유저가 있다고 한다. 이는 User 모델 자체에서 username이 중복되는 User가 생성되지 못하도록 설정해 놓았기 때문이다.
또한 시리얼라이저를 이용해 유저를 생성할 경우, Password에 아무런 규칙이 없어 아주 간단한 비밀번호도 설정할 수 있다.(ex. 12345678) 위에서 실습한 Form의 경우 기본적으로 설정된 규칙이 있었지만, 이 방법을 이용할 경우 보안 문제가 발생할 우려가 있다.
비밀번호 규칙 설정을 위해서는 시리얼라이저의 설정을 약간 수정해주어야 한다. 그리고 비밀번호를 2차 확인하는 함수까지 넣어보자.
# mysite/polls_api/serializers.py
# 비밀번호를 검사하는 메서드 임포트
from django.contrib.auth.password_validation import validate_password
...
# User 모델을 등록하는 Serializer
class RegisterSerializer(serializers.ModelSerializer):
# password 필드를 validate_password 함수를 사용하여 유효성 검사
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
# password와 password2가 일치하는지 검사
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs
class Meta:
model = User
fields = ['username', 'password', 'password2']
validate_password라는 함수를 불러와 password 필드에 기본 규칙을 적용해 유효성 검사를 하도록 만들었다. 또한, password2 필드를 통해 비밀번호 확인하는 함수까지 생성했다. validate에 전달되는 attrs는 시리얼라이저에 전달된 데이터를 포함하는 딕셔너리로, password와 password2 데이터가 포함되어 있다.
이제 서버를 열어 실행해보면,
위와 같이 비밀번호 규칙과 확인란이 잘 동작하는 것을 볼 수 있다.
그러나 모든 규칙을 잘 지킨 상태로 POST를 요청하면
위와 같은 에러가 발생한다. 이는 User 모델에는 username과 password 필드만 존재할 뿐, 위의 경우 password2 필드까지 포함되어 전송하고 있기 때문이다. 이 경우에는 create 메서드를 직접 구현해주어야 한다.
# User 모델을 등록하는 Serializer
class RegisterSerializer(serializers.ModelSerializer):
# password 필드를 validate_password 함수를 사용하여 유효성 검사
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
# password와 password2가 일치하는지 검사
def validate(self, attrs):
# attrs는 serializer에 전달된 데이터를 포함하는 딕셔너리
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "비밀번호 필드가 일치하지 않습니다."})
return attrs
# create 메서드 직접 구현
def create(self, validated_data):
user = User.objects.create(
username=validated_data['username']
)
user.set_password(validated_data['password'])
user.save()
return user
class Meta:
model = User
fields = ['username', 'password', 'password2']
create 메서드를 구현한 뒤 User 생성을 시도하면 정상적으로 처리된다.
User 권한 관리
Question 레코드에도 owner라는 필드로 작성자 이름을 남기고 있는 상태이다. 이제 게시글에도 작성자 이름이 노출되도록 하고, 해당 작성자만 게시글을 수정/삭제할 수 있는 권한을 줄 수 있도록 구현해보자.
Question 생성 시 현재 로그인한 계정이 작성자로 표시되도록 설정하기
settings.py에서 로그인/로그아웃 이후 리다이렉트 할 URL을 먼저 설정해준다.
mysite/settings.py
from django.urls import reverse_lazy
# 로그인/로그아웃 후 리다이렉트할 URL
LOGIN_REDIRECT_URL = reverse_lazy('question_list')
LOGOUT_REDIRECT_URL = reverse_lazy('question_list')
위 코드를 settings.py 상단에 입력한다. (위치는 큰 상관 없다.)
이제 '/question' URL에서 우측 상단 'Log in' 버튼을 눌러 로그인해보자. 이전에 등록한 계정을 활용한다.
user1로 로그인이 정상적으로 처리됐고, question list 페이지로 리다이렉트된 것을 확인할 수 있다.
이제 현재 로그인한 계정이 새로운 Question 레코드를 만들 때, 해당 레코드의 owner가 되도록 설정해보자. 현재 polls_api의 serializers.py에는 QuestionSerializer의 fields가 '__all__'로 설정되어 있기 때문에, 모든 필드가 노출되는 상태다.
이는 질문을 등록할 때도 마찬가지지만, 현재 로그인한 계정이 아닌 Owner를 고를 수 있게 설정되어 있는 상태이다. 현재 로그인한 계정이 질문의 owner가 되도록 고정하려면 어떻게 해야할까?
위에서도 그랬던 것처럼, 우선 owner 필드가 변경되지 않도록 하려면 readonly 옵션을 설정해주어야 한다. 또한 그 리소스는 현재 계정의 username이 되어야 할 것이다. serializers.py를 다음과 같이 수정해보자.
# Question 모델을 보여주는 Serializer
class QuestionSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Question
fields = '__all__'
이제 owner를 고를 수 있는 칸이 사라지고, 질문 칸만 남아있는 것을 알 수 있다. 하지만 아직 owner가 자동으로 지정되는 건 아니다. 임의로 설정할 수 없게 만들었을 뿐이다.
views.py로 이동해 아래 함수를 QuestionList 클래스 내에 추가해주자.
class QuestionList(generics.ListCreateAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
perform_create 함수는 create 메서드가 실행될 때, 시리얼라이저의 owner 정보를 현재 요청한 user의 정보를 받아들여 저장하게 된다. 즉, 질문 생성 시 작성자를 자동으로 설정하게 된다.
이제 POST 요청을 통해 질문을 생성하게 되면, 현재 로그인된 계정인 user1이 자동으로 owner로 지정되는 것을 알 수 있다.
로그인한 계정에 한해서 질문 생성할 수 있도록 설정하기
만약 로그아웃된 상태에서 질문을 만들게되면 어떻게 될까?
위와 같은 에러가 발생하게 된다. Question.owner 필드는 User 인스턴스가 필요하다고 한다.
하지만 사용자에게 이런 에러 화면을 보여줄 수는 없을 것이다. 이런 상황에 대비한 처리가 필요하다.
# mysite/polls_api/views.py
from rest_framework import status, mixins, generics, permissions
class QuestionList(generics.ListCreateAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# 인증된 사용자만 질문 생성 가능
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
# 질문 생성 시 작성자를 자동으로 설정
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
permissions 메서드를 임포트해, permisson_classes에 IsAuthenticatedOrReadOnly를 지정해준다. 이렇게 되면 인증을 받은(로그인 된) 계정에 한해서만 질문을 생성할 수 있고, 그렇지 않다면 읽는 동작만 가능하게 된다.
이후 서버에 다시 접속하게 되면 아래와 같이 입력 창이 사라진 것을 볼 수 있다. (로그아웃 상태)
이후 로그인을 하게 되면 다시 생성 폼이 생기는 것을 확인할 수 있다.
자신이 생성한 Question에 대해서만 수정/삭제 권한 부여하기
현재 세부페이지에 진입했을 때, permission에 의해 로그인한 계정에 한해서만 생성/수정/삭제를 진행할 수 있지만, 다른 계정이 만든 질문에도 해당 권한이 부여되고 있는 상태다.
의도하려는 바와 같이 자신이 생성한 질문에 대해서만 수정/삭제 권한을 부여하기 위해서는 permission을 커스텀하게 만들 필요가 있다. polls_api 폴더 내 permissions.py를 새로 만들어보자.
# mysite/polls_api/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# SAFE_METHODS는 GET, OPTIONS, HEAD 메서드(읽기 전용)
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
세부 페이지에 접속했을 때, 해당 질문의 owner인지 아닌지에 대해 판단하는 클래스이다. 우선 SAFE_METHOD인 GET, OPTIONS, HEAD 등 읽는 동작 요청에 대해서는 True로 전달해주며, 이 외의 요청(PUT, DELETE 등)은 질문의 owner와 요청하는 owner가 동일할 때만 True로 전달해준다.
이를 views.py의 QuestionDetail에 적용해준다.
from .permissions import IsOwnerOrReadOnly
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
이후 서버를 확인하면, User1로 로그인한 상태에서 자신이 만든 질문에는 PUT, DELETE 기능이 노출되지만,
User3으로 접속했을 때는 아무 폼도 보이지 않는 것을 알 수 있다.
'Minding's Programming > Django' 카테고리의 다른 글
[Django] 투표 기능 구현하기 (0) | 2024.10.11 |
---|---|
[Django] RelatedField (0) | 2024.10.11 |
[Django] 클래스 기반의 View, Mixin, Generic View (0) | 2024.10.10 |
[Django] GET, POST, PUT, DELETE (1) | 2024.10.10 |
[Django] Serilaizer (4) | 2024.10.10 |