W | eb

Django CRUD: HTTP1

덞웖이 2024. 10. 9. 17:59

환경

- Ubuntu Server 22.04 
- VSCode Insider 
- Remote SSH Extension 
- Postman Extension
- Python 3.10 venv
- Django 5.1.1
- SQLite 3.37.2

준비

# migration 안해도 되나 ??...

source django_env/bin/activate

mkdir test_proj/polls/templates/registration
touch test_proj/polls/templates/registration/signup.html
touch test_proj/polls_api/permissions.py

# @test_proj/settings.py에 추가
INSTALLED_APPS = ['polls_api.apps.PollsApiConfig'] # 근데 어디에 씀

유저

더보기

polls/models.py

### 1. 모델에 User 추가: 기본 제공하는 django.contrib.auth 사용
class Question(models.Model):
	# ...
    # [User] 1 ㅡ * [Question] 1 ㅡ * [Choice] 라는 설명인듯 ...?
    # related name 설정하면 쿼리 할 때 question_set과 같은 default 이름 대신 다른 이름 사용 가능
    owner = models.ForeignKey('auth.User', related_name='questions', on_delete=models.CASCADE, null=True)
    # ...

Django Shell 테스트

python manage.py makemigrations
python manage.py migrate
# 0002_question_owner migration 확인

python manage.py shell

# shell
from django.contrib.auth.models import User
from polls.models import *

user = User.objects.first()
# related_name 파라미터에서 정해준 이름 사용하는 것 확인
print(user.questions.all().query)
# 쿼리에 유저 id 조건이 붙음...

 


유저 관리

더보기

polls_api/serializers.py

# 유저 관리 기능을 chooga 해보자
# ...
from django.contrib.auth.models import User

# ...
class UserSerializer(serializers.ModelSerializer):
	# many -> 여러개의 question
	questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'questions']
# ...

polls_api/views.py

# ...
from polls_api.serializers import QuestionSerializer, UserSerializer
from django.contrib.auth.models import User
# ...

# ...
class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
# ...

polls_api/urls.py

# ...
urlpatterns = [
	# ...
    path('users/', UserList.as_view(), name='user-list'), # name은 유저 생성에서 사용
    path('users/<int:pk>', UserDetail.as_view(), name='user-detail'),
]
# ...

유저 생성 (django form method)

더보기

polls/views.py

# ...
from django.views import generic # rest_framework의 generics와 유사함
from django.urls import reverse_lazy
from django.contrib.auth.forms import UserCreationForm
# ...

# ...
class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    # 생성 성공시 redirect 페이지.
    # 다른 앱/urls.py에 있는 name을 그냥 넣어도 되네
    success_url = reverse_lazy('user-list') # urls.py 의 name으로 url 만듬
    template_name = 'registration/signup.html'
# ...

polls/templates/registration/signup.html

<h2>sign up</h2>
<form method="post">
    {% csrf_token %}
    <!-- views.py의 form 클래스 사용 -->>
    {{ form.as_p }}
    <button type="submit">sign up</button>
</form>

polls/urls.py

# ...
from .views import *
# ...

# ...
urlpatterns = [
   	# ...
    path('signup/', SignupView.as_view()),
]

유저 생성 (api method)

더보기

polls_api/serializer.py

# ...
# 이 방법에서는 validate 따로 해줘야 함 (중복 아이디 제외)
from django.contrib.auth.password_validation import validate_password
# ...

# ...
class RegisterSerializer(serializers.ModelSerializer):
	# 비번 재입력 확인
    password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
    password2 = serializers.CharField(write_only=True, required=True)
    # ModelSerializer 오버라이드 (run_validations에서 실행됨)
    def validate(self, attrs):
        if attrs['password'] != attrs['password2']:
            raise serializers.ValidationError({'password': 'incorrect confirm pw'})
        return attrs
    class Meta:
        model = User
        fields = ['username', 'password', 'password2']
        # validator 추가 후에 필요 없어짐
        # extra_kwargs = {'password': {'write_only': True}}
# ...

polls_api/views.py

# ...
from polls_api.serializers import * # 수정
# ...

# ...
# list 볼 필요 없고 post 만 하면 되니까 createview로
class RegisterUser(generics.CreateAPIView):
    serializer_class = RegisterSerializer

polls_api/urls.py

# ...
urlpatterns = [
    # ...
    path('register/', RegisterUser.as_view()),
]
# ...

유저 로그인

더보기

polls_api/urls.py

from django.urls import path, include
# ...

# ...
urlpatterns = [
    # ...
    path('api-auth/', include('rest_framework.urls'))
]

test_proj/settings.py

# ...
from django.urls import reverse_lazy

LOGIN_REDIRECT_URL = reverse_lazy('question-list')
LOGOUT_REDIRECT_URL = reverse_lazy('question-list')
# ...

이게 끝이라고 ? 🙀


유저 권한 (리소스 소유)

더보기

polls_api/serializers.py

# ...
class QuestionSerializer(serializers.ModelSerializer):
    # 없으면 owner 선택지 생김
    owner = serializers.ReadOnlyField(source='owner.username')
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date', 'owner']
# ...

polls_api/permissions.py

# 다른 이용자 글 수정 방지를 위해 custom permission 따로 만든다
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS: # read only related methods
            return True
        return obj.owner == request.user

polls_api/views.py

# ...
from rest_framework import generics, permissions # 추가
class QuestionList(generics.ListCreateAPIView):
	
    # ...
    # auth 없을 때 쓰기 방지
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    # 로그아웃시 입력창이 아예 사라짐
    # ...
    
    # session에 유저 추가 (기존 create에 owner 붙여서 오버라이드)
    # 여기까지 해야 question에 owner가 붙음
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
    
    # ...
    # permission 추가로 다른 이용자가 편집하는 것 방지
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
    
# ...
본인 질문에만 인풋 영역이 생기는 것 확인

Misc, Thoughts

더보기
# shell에서
qs = QuestionSerializer(data={'question_text': 'xxx', 'owner': 'aaa'})
qs.is_valid()
qs.validated_data
# 를 살펴보면 owner가 들어가지 않음 (read_only이므로)

# save는 강제로 집행 가능
q = qs.save(id=1000)
q.id

# 그래서 polls_api/views.py에서 perform_create처럼 작동 가능
# (serializer.save(owner=self.request.user))

# 브라우저 데브툴에서 Application - cookies에서 해당 사이트 세션id 확인 가능
# -> postman 테스트에 사용
Postman: Django의 credential 헤더들
CRUD is short for Crush, Ruin, Undermine and Destroy