본문 바로가기
웹/백엔드

파트 4~5: Django 명령어 모음

by 보안매크로 2024. 10. 20.
728x90

User 추가하기

[강의 소스코드]

  • 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='생성일')  
    owner = models.ForeignKey('auth.User', related_name='questions', on_delete=models.CASCADE, null=True)
                              
    @admin.display(boolean=True, description='최근생성(하루기준)')
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
    
    def __str__(self):
        return f'제목: {self.question_text}, 날짜: {self.pub_date}'
    ...
  • Django Shell
>>> from django.contrib.auth.models import User
>>> User
<class 'django.contrib.auth.models.User'>
>>> User._meta.get_fields()
>>> User.objects.all()
<QuerySet [<User: admin>]>

>>> from polls.models import * 
>>> user = User.objects.first()
>>> user.questions.all() 
>>> 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 관리하기

[강의 소스코드]

  • polls_api/serializers.py
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
    
    class Meta:
        model = User
        fields = ['id', 'username', 'questions']
  • polls_api/views.py
from django.contrib.auth.models import User
from polls_api.serializers import UserSerializer

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
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', QuestionList.as_view(), name='question-list'),
    path('question/<int:pk>/', QuestionDetail.as_view()),
    path('users/', UserList.as_view(),name='user-list'),
    path('users/<int:pk>/', UserDetail.as_view()),

]

 

Form을 사용하여 User 생성하기

[강의 소스코드]

  • polls/views.py
from django.views import generic
from django.urls import reverse_lazy
from django.contrib.auth.forms import UserCreationForm

class SignupView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy('user-list')
    template_name = 'registration/signup.html'

  • polls/templates/registration/signup.html
<h2>회원가입</h2>
 <form method="post">
   {% csrf_token %}
   {{ form.as_p }}
   <button type="submit">가입하기</button>
</form>
  • polls/urls.py
from django.urls import path
from . import views
from .views import *

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>/result/', views.result, name='result'), 
    path('signup/', SignupView.as_view(), )
]
  • Django Shell
>>> from django.urls import reverse_lazy
>>> reverse_lazy('user-list')
'/rest/users/'

 

Serializer를 사용하여 User 생성하기

[강의 소스코드]

  • polls_api/serializers.py
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
    
    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']
  • polls_api/views.py
from polls_api.serializers import RegisterSerializer

class RegisterUser(generics.CreateAPIView):
    serializer_class = RegisterSerializer
  • polls_api/urls.py
from django.urls import path
from .views import *

urlpatterns = [
    path('question/', QuestionList.as_view(), name='question-list'),
    path('question/<int:pk>/', QuestionDetail.as_view()),
    path('users/', UserList.as_view(),name='user-list'),
    path('users/<int:pk>/', UserDetail.as_view()),
    path('register/', RegisterUser.as_view()),

]

 

User 권한 관리

[강의 소스코드]

  • polls_api/urls.py
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()),
    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'))

]
  • mysite/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 = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date', 'owner']
  • polls_api/permissions.py
from rest_framework import permissions

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
  • polls_api/views.py
from rest_framework import generics,permissions
from .permissions import IsOwnerOrReadOnly

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)

class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

 

상속(Inheritance) 과 오버라이딩(Overriding )

파이썬에서는 상속(Inheritance)이라는 개념이 존재합니다. 부모가 자식에게 재산이나 지위를 물려준다는 상속의 의미처럼, 하나의 클래스가 다른 클래스로부터 메소드와 속성을 그대로 물려받아 사용할 수 있는 기능을 제공합니다. 이 때 자신의 메소드와 속성을 물려주는 클래스를 부모 클래스(Parent Class) 또는 상위 클래스(Super class)라고 지칭합니다. 반대로 그것을 물려받아서 사용하는 클래스는 자식 클래스(Child Class) 또는 하위 클래스(Sub Class)라고 부릅니다.

파이썬에서 상속을 구현하기 위한 방법은 자식 클래스의 괄호 안에 부모 클래스를 명시하는 것 입니다. 그렇게하면 자식 클래스에서는 부모 클래스의 모든 메서드와 속성을 자유롭게 사용할 수 있으며, 그에 더해 새로운 메서드를 추가하거나 기존 메서드의 내용을 변경하는 것도 가능합니다.

다음은 부모 클래스인 Animal 클래스를 정의하고, 자식 클래스인 Dog 클래스가 Animal 클래스를 상속받도록 구현하는 예시코드입니다.

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "동물이 울음소리를 냅니다"

class Dog(Animal):
    def speak(self):
        return "멍멍!"

my_dog = Dog("초코")
print(my_dog.name)  # 출력: 초코
print(my_dog.speak())  # 출력: 멍멍!

위 예시에서 Dog 클래스는 Animal 클래스를 상속받았으므로, Animal 클래스의 모든 속성과 메서드를 상속받습니다. 특히 Dog 클래스에서 speak 메서드의 내용을 변경하여 재정의하였기 때문에, speak 메서드를 호출하였을 때 멍멍!이라는 결과가 출력되는 것을 확인할 수 있습니다.
바로 이와 같이 상속받은 메서드의 내용을 자식 클래스에서 변경하여 사용하는 것을 오버라이딩(Overriding )이라고 합니다. 상속받은 메서드의 기능을 그대로 사용할 때도 있지만, 자식 클래스에서 필요에 맞게 변경해서 사용해야 하는 경우가 많습니다. 이런 경우에 오버라이딩을 사용하면 같은 이름의 메서드를 자식 클래스에서 다른 기능으로 재정의하여 사용할 수 있습니다.

상속을 활용하면 코드의 재사용성을 높일 수 있고, 클래스간의 계층 구조를 만들어 관련성 있는 클래스들을 그룹화하여 관리 할 수 있다는 장점이 있습니다. 또한, 부모 클래스의 코드를 다시 반복하여 작성할 필요가 없어 새로운 클래스를 작성하는 데 필요한 코드의 양을 줄일 수 있습니다.

이후에 배우게 될 부분에서는 파이썬 상속과 관련된 내용들이 다뤄질 예정입니다. 그에 앞서 몇가지 코딩 실습 문제들을 풀면서 파이썬의 상속에 대해 학습해 보세요.

 

perform_create()

[강의 소스코드]

  • Django Shell
>>> from polls_api.serializers import QuestionSerializer
>>> question_serializer = QuestionSerializer(data={"question_text": "some text","owner" :"someone"})
>>> question_serializer.is_valid()
True
>>> question_serializer.validated_data
OrderedDict([('question_text', 'some text')])
>>> question = question_serializer.save(id=10000)
>>> question.id
10000
>>> question.question_text
'some text'

16. RelatedField

[멘트 정정안내]
강의 6분 27초에서 "Choice를 불러올 때는 그 이름을 questions로 불러오도록" 이라고 잘못 전달드린 내용의 멘트가 있어서 정정합니다.
해당 멘트의 올바른 내용은 "Choice를 불러올 때는 그 이름을 choices로 불러오도록" 이 되어야 합니다.
Choice를 불러올 때는 questions 가 아니라, choices 라는 related_name을 사용해야한다는 점을 기억해주세요.

[강의 소스코드]

  • polls_api/serializers.py
class ChoiceSerializer(serializers.ModelSerializer): 
    class Meta:
        model = Choice
        fields = ['choice_text', 'votes']

class QuestionSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    choices = ChoiceSerializer(many=True, read_only=True)
    
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date', 'owner', 'choices']

class UserSerializer(serializers.ModelSerializer):
    #questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
    #questions = serializers.StringRelatedField(many=True, read_only=True)
    #questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
    questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
    
    class Meta:
        model = User
        fields = ['id', 'username', 'questions']
  • polls_api/urls.py
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'))

]
  • polls/models.py
...
class Choice(models.Model):
    question = models.ForeignKey(Question, related_name='choices', on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text

 

17. 투표(Votes) 기능 구현하기 1 - Models

[강의 소스코드]

  • polls/models.py
from django.contrib.auth.models import User

class Vote(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice = models.ForeignKey(Choice, on_delete=models.CASCADE)
    voter = models.ForeignKey(User, on_delete=models.CASCADE)
    
    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['question', 'voter'], name='unique_voter_for_questions')
        ]
  • polls_api/serializers.py
class ChoiceSerializer(serializers.ModelSerializer):
    votes_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Choice
        fields = ['choice_text', 'votes_count']
        
    def get_votes_count(self, obj):
        return obj.vote_set.count()
  • Django Shell
>>> from polls.models import *
>>> question = Question.objects.first()
>>> choice = question.choices.first()
>>> from django.contrib.auth.models import User
>>> user= User.objects.get(username='luke')
>>> Vote.objects.create(voter=user,question=question,choice=choice)
<Vote: Vote object (1)>
>>> question.id
1

 

18. 투표(Votes) 기능 구현하기 2 - Serializers & Views

[강의 소스코드]

  • polls_api/serializers.py
from polls.models import Question,Choice, Vote

class VoteSerializer(serializers.ModelSerializer):    
    voter = serializers.ReadOnlyField(source='voter.username')
        
    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
  • polls_api/views.py
from polls.models import Question,Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly , IsVoter

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_queryset(self, *args, **kwargs):
        return Vote.objects.filter(voter=self.request.user)
   
    def perform_create(self, serializer):
        serializer.save(voter=self.request.user)
  
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsVoter]
  • polls_api/permissions.py
class IsVoterOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.voter == request.user
  • polls_api/urls.py
from django.urls import path, include
from .views import VoteList, VoteDetail

urlpatterns = [
    ...
    path('vote/', VoteList.as_view()),
    path('vote/<int:pk>/', VoteDetail.as_view()),
]

 

 

19. Validation

[강의 소스코드]

  • polls_api/serializers.py
from rest_framework.validators import UniqueTogetherValidator

class VoteSerializer(serializers.ModelSerializer):
    def validate(self, attrs):
        if attrs['choice'].question.id != attrs['question'].id:
            raise serializers.ValidationError("Question과 Choice가 조합이 맞지 않습니다.")
        
        return attrs
    
    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
        validators = [
            UniqueTogetherValidator(
                queryset=Vote.objects.all(),
                fields=['question', 'voter']
            )
        ]
  • polls_api/views.py
from rest_framework import status
from rest_framework.response import Response

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_queryset(self, *args, **kwargs):
        return Vote.objects.filter(voter=self.request.user)
    
    def create(self, request, *args, **kwargs):
        new_data = request.data.copy()
        new_data['voter'] = request.user.id
        serializer = self.get_serializer(data=new_data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
    
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated, IsVoter]
    
    def perform_update(self, serializer):
        serializer.save(voter=self.request.user)

 

20. Testing

[강의 소스코드]

  • polls_api/test.py
from django.test import TestCase
from polls_api.serializers import QuestionSerializer

class QuestionSerializerTestCase(TestCase):
    def test_with_valid_data(self):
        serializer = QuestionSerializer(data={'question_text': 'abc'})
        self.assertEqual(serializer.is_valid(), True)
        new_question = serializer.save()
        self.assertIsNotNone(new_question.id)
        
    def test_with_invalid_data(self):
        serializer = QuestionSerializer(data={'question_text': ''})
        self.assertEqual(serializer.is_valid(), False)
  • 테스트 실행하기
python manage.py test

 

21. Testing Serializers

[강의 소스코드]

  • polls_api/tests.py
class VoteSerializerTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(username='testuser')
        self.question = Question.objects.create(
            question_text='abc',
            owner=self.user,
        )
        self.choice = Choice.objects.create(
            question=self.question
            choice_text='1'
        )    
    
   def test_vote_serializer(self):  
        self.assertEqual(User.objects.all().count(), 1)
        data = {
            'question': self.question.id
            'choice': self.choice.id
            'voter': self.user.id
        }
        serializer = VoteSerializer(data=data)
        self.assertTrue(serializer.is_valid())
        vote = serializer.save()
    
        self.assertEqual(vote.question, self.question)
        self.assertEqual(vote.choice, self.choice)
        self.assertEqual(vote.voter, self.user)
    
    def test_vote_serializer_with_duplicate_vote(self):
        self.assertEqual(User.objects.all().count, 1)
        choice1 = Choice.objects.create(
            quetsion=self.question,
            choice_text='2'
        )
        Vote.objects.create(question=self.question, choice=self.choice, voter=self.user)
    
        data = {
            'question': self.question.id
            'choice': self.choice.id
            'voter': self.user.id
        }
        serializer = VoteSerializer(data=data)
        self.assertTrue(serializer.is_valid())
    
    def test_vote_serilaizer_with_unmatched_question_and_choice(self):
        question2 = Question.objects.create(
            question_text='abc',
            owner=self.user,
        )
    
        choice2 = Choice.objects.create(
            quetsion=question2,
            choice_text='1'
        )
        data = {
            'question': self.question.id
            'choice': self.choice.id
            'voter': self.user.id
        }
        serializer = VoteSerializer(data=data)
        self.assertTrue(serializer.is_valid())

 

22. Testing Views

[강의 소스코드]

  • polls_api/tests.py
from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status
from django.utils import timezone

class QuestionListTest(APITestCase):
    def setUp(self):
        self.question_data = {'question_text': 'some question'}
        self.url = reverse('queston-list')
    
    def test_create_question(self):
        user =User.objects.create(username='testuser', password='testpass')
        self.client.force_authenticate(user=user)
        response = self.client.post(self.url, self.question_data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Question.objects.count(), 1)
        question = Question.objects.first()
        self.assertEqual(question.question_text, self.question_data['question_text'])
        self.assertEqual((timezone.now - question.pub_date).total_seconds(), 1)
    
    def test_create_question_without_authentication(self):
        response = self.client.post(self.url, self.question_data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
    def test_list_question(self):
        question = Question.objects.create(question_text='Question1')
        choice = Choice.objects.create(question=question, choice_text='Question1')
        Question.objects.create(question_text='Question2')
        response = self.client.post(self.url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 2)
        self.assertEqual(response.data[0]['choices'][0]['choice_text'], choice.choice_text)
  • Python 코드 커버리지 라이브러리 설치하기
pip install coverage
  • Python 코드 커버리지 라이브러리 실행하기
coverage run manage.py test
728x90