장고에서는 특정 모델(테이블)에 대한 유저를 정의할 수가 있습니다.
코드와 함께 예시를 살펴 보면 다음과 같습니다.
from django.db import models
from django.utils import timezone
import datetime
from django.contrib import admin
class Question(models.Model):
# verbose_name 옵션에 필드의 칼럼명 입력
question_text = models.CharField(max_length=200, verbose_name="질문") # 텍스트를 저장하는 필드
pub_date = models.DateTimeField(auto_now_add=True, verbose_name="생성일") # 날짜를 저장하는 필드
# 해당 질문의 owner를 정의.
# owner가 삭제되면 Qeustion은 모두 delete(CASCADE)되며,
# 필드가 비어 있어도(null) 괜찮다는 의미.
# 유저의 Question을 불러올 때, realated_name인 'questions'로 가져옴.
owner = models.ForeignKey('auth.User', related_name='questions', on_delete=models.CASCADE, null=True)
# @admin.display 어노테이션으로 함수의 칼럼명 입력
@admin.display(boolean=True, description='최근생성(하루기준)')
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE) # 외래키 Question을 CASCADE로 참조한다.
choice_text = models.CharField(max_length=200) # 텍스트를 저장하는 필드
votes = models.IntegerField(default=0) # 숫자를 저장하는 필드
def __str__(self):
return self.choice_text
터미널에서 python manage.py makemigrations
명령어를 입력 후, python manage.py migrations
명령어를 입력합니다.
Django Shell에서 여러 값들을 확인해 봅니다.
# 장고에서 제공하는 User 모델을 임포트합니다.
>>> from django.contrib.auth.models import User
>>> User
<class 'django.contrib.auth.models.User'>
# User 모델의 정보를 확인해 봅니다.
# id, password,last_login 등의 필드를 가지고 있습니다.
>>> 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.objects.all()
<QuerySet [<User: admin>, <User: admin2>]>
# 이제 저희가 정의한 모델과 함께 비교해 봅니다.
>>> from polls.models import *
>>> user = User.objects.first()
>>> user.questions.all()
<QuerySet []>
>>> print(user.questions.all())
<QuerySet []>
# 해당 쿼리문이 어떻게 되어있는지 확인해 봅니다.
# WHERE 절에서 owner_id가 1인 것으로 묶고 있는 것을 확인할 수가 있습니다.
>>> 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
# 해당 유저의 id가 1인 것을 확인할 수가 있습니다.
>>> user.id
1
>>> question = Question.objects.first()
# 해당 질문의 선택지 객체 목록을 확인합니다.
>>> question.choice_set.all()
<QuerySet [<Choice: 바다>, <Choice: 강>]>
# 해당 쿼리문을 확인해 봅니다.
# WHERE 절에서 choice의 question의 id가 1인 것들을 갖고 오는 것을 확인할 수 있습니다.
>>> print(question.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
위에서 보이는 것처럼, Question에 대해서 쿼리를 할 때는 owner_id를 기준으로 쿼리를 하고, Choice에 대해서 쿼리를 할 때는 question__id를 기준으로 쿼리를 하고 있다는 것을 알 수가 있습니다.
이유는 Question과 Choice 모델이 각각 User와 Question를 ForeignKey로 참조하고 있기 때문입니다.
즉,
{ 한 명의 User : 하나 이상의 Question }
{ 하나의 Question : 하나 이상의Choice } 로 볼 수 있습니다.
User에 대한 Serializer와 View를 정의해 봅니다.
절차는 다음과 같습니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question
# User 모델 임포트
from django.contrib.auth.models import User
# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = Question
fields = ['id', 'question_text', 'pub_date']
# 추가된 UserSerializer
class UserSerializer(serializers.ModelSerializer):
# User 모델의 Primary Key 값으로 연결된 모든 Question 모델의 오브젝트들을 가져옴
questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'questions']
from polls.models import Question
from polls_api.serializers import QuestionSerializer, UserSerializer
from rest_framework import generics
from django.contrib.auth.models import User
# generics 클래스의 ListCreateAPIView 상속
# get, post 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# User 목록 조회 뷰
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
# 특정 User 상세 페이지 조회 뷰
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
from django.urls import path
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view(), name='question_detail'),
# 추가된 경로들
path('users/', UserList.as_view()),
path('users/<int:pk>/', UserDetail.as_view()),
]
장고에서 제공하는 Form 기능을 이용하여 User를 만들 수 있습니다. 장고에서 제공하는 기능은 polls/ 에 구현하였습니다.
절차는 다음과 같습니다.
from django.urls import path
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view(), name='question_detail'),
path('users/', UserList.as_view(), name='user-list'),
path('question/<int:pk>/', QuestionDetail.as_view, name='question_detail'),
]
from django.http import HttpResponse, HttpResponseRedirect
from .models import *
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.db.models import F
# generic 뷰 모델 임포트
from django.views import generic
# url을 생성하는 revese_lazy 메서드 임포트
from django.urls import reverse_lazy
# 유저생성폼 임포트
from django.contrib.auth.forms import UserCreationForm
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'questions' : latest_question_list}
return render(request, 'polls/index.html', context)
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {'question': question, 'error_message': f"선택이 없습니다. id={request.POST['choice']}"})
else:
# F : DB에서 읽는다는 의미.
selected_choice.votes = F('votes') + 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:result', args=(question.id,)))
def result(request, question_id):
question = get_object_or_404(Question, id=question_id)
return render(request, 'polls/result.html', {'question' : question})
# 회원가입 뷰 클래스 정의
class SignUpView(generic.CreateView):
form_class = UserCreationForm
# 회원 가입 성공 시, success_url로 이동(리디렉션)함.
success_url = reverse_lazy('user-list')
# 템플릿을 정의함
template_name = 'registration/signup.html'
<h2>회원가입<h2>
<form method="post">
{% csrf_token %}
<!-- views.py의 form_class를 그대로 받아옴. -->
{{ form.as_p }}
<button type="submit">가입하기</submit>
</form>
장고 rest_framework에서 제공하는 Serializer 기능을 이용해서도 User를 만들 수 있습니다. django rest framwork에서 제공하는 기능은 polls/ 에 구현하였습니다. 절차는 다음과 같습니다.
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question
# User 모델 임포트
from django.contrib.auth.models import User
# 간단한 패스워드 가입에 대한 거부 처리를 위한 임포트
from django.contrib.auth.password_validation import validate_password
# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = Question
fields = ['id', 'question_text', 'pub_date']
class UserSerializer(serializers.ModelSerializer):
# User 모델의 Primary Key 값으로 연결된 모든 Question 모델의 오브젝트들을 가져옴
questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'questions']
# 사용자 등록 시리얼라이저 정의
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
# 패스워드 재확인용
password2 = serializers.CharField(write_only=True, required=True)
# 패스워드 재확인 메서드
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({'password' : '두 패스워드가 일치하지 않습니다.'})
return attrs
# User에는 password2가 정의되어 있지 않기 때문에 error가 발생.
# 따라서 직접 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']
from polls.models import Question
from polls_api.serializers import *
from rest_framework import generics
from django.contrib.auth.models import User
# generics 클래스의 ListCreateAPIView 상속
# get, post 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
# 회원 등록 뷰 클래스 정의
class RegisterUser(generics.CreateAPIView):
serializer_class = RegisterSerializer
from django.urls import path
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view(), name='question_detail'),
path('users/', UserList.as_view(), name='user-list'),
path('users/<int:pk>/', UserDetail.as_view()),
path('register/', RegisterUser.as_view()),
]
다음과 같은 기능들에 대해서 구현을 해 봅니다.
위 기능들을 모두 코드로 구현하면 다음과 같습니다.
from django.urls import path, include
from .views import *
urlpatterns = [
path('question/', QuestionList.as_view(), name='question-list'),
path('question/<int:pk>/', QuestionDetail.as_view(), name='question_detail'),
path('users/', UserList.as_view(), name='user-list'),
path('users/<int:pk>/', UserDetail.as_view()),
path('register/', RegisterUser.as_view()),
path('api-auth', include('rest_framework.urls')),
]
...
from django.urls import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('question-list')
LOGOUT_REDIRECT_URL = reverse_lazy('question-list')
...
# Django의 REST 프레임워크에서 시리얼라이저 임포트.
from rest_framework import serializers
from polls.models import Question
# User 모델 임포트
from django.contrib.auth.models import User
# 간단한 패스워드 가입에 대한 처리를 위해 임포트
from django.contrib.auth.password_validation import validate_password
# ModelSerializer를 상속받는 경우, 필드 및 메서드가 자동으로 지정됨.
class QuestionSerializer(serializers.ModelSerializer):
# 사용자의 이름으로 읽어서 해당 owner를 read only로 갖고 옴.
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Question
fields = ['id', 'question_text', 'pub_date', 'owner']
class UserSerializer(serializers.ModelSerializer):
# User 모델의 Primary Key 값으로 연결된 모든 Question 모델의 오브젝트들을 가져옴
questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'questions']
# 사용자등록 시리얼라이저 정의
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
# 패스워드 재확인용
password2 = serializers.CharField(write_only=True, required=True)
# 패스워드 재확인 메서드
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({'password' : '두 패스워드가 일치하지 않습니다.'})
return attrs
# User에는 password2가 정의되어 있지 않기 때문에 직접 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']
# 특정 질문에 대한 수정 권한을 관리하기 위한 파일.
from rest_framework import permissions
# owner이거나 owner가 아니라면 읽기용으로만 접근 가능.
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
from polls.models import Question
from polls_api.serializers import *
# 로그아웃된 상태에서 post를 하지 못하도록 막기 위해 permissions 임포트
from rest_framework import generics, permissions
from django.contrib.auth.models import User
from .permissions import IsOwnerOrReadOnly
# generics 클래스의 ListCreateAPIView 상속
# get, post 기능이 구현되어 있음
class QuestionList(generics.ListCreateAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# 로그인된 상태에서만 질문 등록이 가능하게 만듦.
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
# QuestionList 뷰에서 create할 때, owner는 현재 로그인된 user로
# 지정되어서 create 됨.
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
# generics 클래스의 RetrieveUpdateDestroyAPIView 상속
# get, put, delete 기능이 구현되어 있음.
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# 로그인된 상태에서만 질문 수정이 가능하게 만듦.
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
# 회원 등록 뷰 클래스 정의
class RegisterUser(generics.CreateAPIView):
serializer_class = RegisterSerializer
perform_create는 여러 상속을 거쳐 overriding된 메서드입니다. 호출 순서: post() -> crete() -> perform_create(). 따라서, 위의 코드에서는, owner가 해당 유저가 되도록 perform_create 메서드를 overring한 것입니다.
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
그리고 serializer.save()를 호출할 때, owner를 지정한 것처럼 read only와 같은 필드를 별도로 지정할 수도 있습니다.