섹션3: Django REST Framework - Level one

천호영·2021년 6월 10일
0

Introduction to DRF and NewsAPI Project Setup

venv와 docker 환경 설정

python3 -m venv venv
source venv/bin/activate
pip install django
pip install djangorestframework
pip install psycopg2

#Dockerfile 작성
#docker-compse.yml 작성

docker build . #docker build -t app . 으로 이미지에 이름붙이기
docker-compose run web django-admin startproject newsapi .

sudo chown -R user ./ #수정권한 부여
#settings.py에서 DATABASES 설정
docker-compose up -d #서비스 실행 후 콘솔로 빠져나오기

docker-compose exec web python manage.py startapp news
#startapp하고 꼭 바로 INSTALLED_APPS에 추가!
#'rest_framework'도 잊지말고 추가!

models.py 작성

#news/models.py

from django.db import models


class Article(models.Model):
    author = models.CharField(max_length=50)
    title = models.CharField(max_length=120)
    description = models.CharField(max_length=200)
    body = models.TextField()
    location = models.CharField(max_length=120)
    publication_date = models.DateField()
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.author} {self.title}"

makemigrations & migrate

docker-compose exec web python manage.py makemigrations
docker-compose exec web python manage.py migrate

createsuperuser

docker-compose exec web python manage.py createsuperuser

admin.py register & runserver

#news/admin.py

from django.contrib import admin
from news.models import Article

admin.site.register(Article)
docker-compose exec web python manage.py runserver

What are the Serializers?

먼저, serializers.py를 작성합니다.

#news/api/serializers.py

from rest_framework import serializers
from news.models import Article


class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    author = serializers.CharField()
    title = serializers.CharField()
    description = serializers.CharField()  # model에서 TextField
    body = serializers.CharField()
    location = serializers.CharField()
    publication_data = serializers.DateField()
    active = serializers.BooleanField()
    created_at = serializers.DateTimeField(read_only=True)
    updated_at = serializers.DateTimeField(read_only=True)

    def create(self, validated_data):
        print(validated_data)
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.author = validated_data.get('author', instance.author)
        instance.title = validated_data.get('title', instance.title)
        instance.description = validated_data.get(
            'description', instance.description)
        instance.body = validated_data.get('body', instance.body)
        instance.location = validated_data.get('location', instance.location)
        instance.publication_data = validated_data.get(
            'publication_data', instance.publication_data)
        instance.active = validated_data.get('active', instance.active)
        instance.save()
        return instance

그리고 shell을 통해 serialization과 deserialization과정을 살펴봅니다.

#django shell

>>> from news.models import Article
>>> from news.api.serializers import ArticleSerializer
>>> article_instance = Article.objects.first()
>>> article_instance
<Article: John Doe Happy Birthday>


# complex data(e.g.queryset, model instances) -> native Python data type
>>> serializer = ArticleSerializer(article_instance)
>>> serializer
ArticleSerializer(<Article: John Doe Happy Birthday>):
    id = IntegerField(read_only=True)
    author = CharField()
    title = CharField()
    description = CharField()
    body = CharField()
    location = CharField()
    publication_date = DateField()
    active = BooleanField()
    created_at = DateTimeField(read_only=True)
    updated_at = DateTimeField(read_only=True)
>>> serializer.data
{'id': 1, 'author': 'John Doe', 'title': 'Happy Birthday', 'description': 'some fancy description', 'body': 'content', 'location': 'Earth', 'publication_date': '2021-06-10', 'active': True, 'created_at': '2021-06-10T06:35:39.175145Z', 'updated_at': '2021-06-10T06:35:39.175167Z'}


# native Python data types -> JSON
>>> from rest_framework.renderers import JSONRenderer
>>> json = JSONRenderer().render(serializer.data)
>>> json
b'{"id":1,"author":"John Doe","title":"Happy Birthday","description":"some fancy description","body":"content","location":"Earth","publication_date":"2021-06-10","active":true,"created_at":"2021-06-10T06:35:39.175145Z","updated_at":"2021-06-10T06:35:39.175167Z"}'

# JSON -> native Python data types
>>> import io
>>> from rest_framework.parsers import JSONParser
>>> stream = io.BytesIO(json)
>>> data = JSONParser().parse(stream)
>>> data
{'id': 1, 'author': 'John Doe', 'title': 'Happy Birthday', 'description': 'some fancy description', 'body': 'content', 'location': 'Earth', 'publication_date': '2021-06-10', 'active': True, 'created_at': '2021-06-10T06:35:39.175145Z', 'updated_at': '2021-06-10T06:35:39.175167Z'}

# native Python data types -> complex data(e.g.queryset, model instances)
>>> serializer = ArticleSerializer(data=data)
>>> serializer.is_valid()
True
>>> serializer.validated_data
OrderedDict([('author', 'John Doe'), ('title', 'Happy Birthday'), ('description', 'some fancy description'), ('body', 'content'), ('location', 'Earth'), ('publication_date', datetime.date(2021, 6, 10)), ('active', True)])
>>> serializer.save()
{'author': 'John Doe', 'title': 'Happy Birthday', 'description': 'some fancy description', 'body': 'content', 'location': 'Earth', 'publication_date': datetime.date(2021, 6, 10), 'active': True}
<Article: John Doe Happy Birthday>

#save()를 통해 Article객체 추가됨
>>> Article.objects.all()
<QuerySet [<Article: John Doe Happy Birthday>, <Article: John Doe Happy Birthday>]>

The @api_view Decorator - Part One

@api_view를 통해 function based view를 작성합니다.

#news/api/views.py

from rest_framework import serializers, status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from news.models import Article
from news.api.serializers import ArticleSerializer


@api_view(["GET", "POST"])
def article_list_create_api_view(request):

    if request.method == "GET":
        articles = Article.objects.filter(active=True)
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    elif request.method == "POST":
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

api view에 해당되는 endpoint를 설정합니다.

#news/api/urls.py

from django.urls import path
from news.api.views import article_list_create_api_view

urlpatterns = [
    path("articles/", article_list_create_api_view, name="article-list")

]

The @api_view Decorator - Part Two

GET, PUT, DELETE에 해당하는 부분까지 모두 구현합니다.

#news/api/views.py

@api_view(["GET", "PUT", "DELETE"])
def article_detail_api_view(request, pk):
    try:
        article = Article.objects.get(pk=pk)
    except Article.DoesNotExist:
        return Response({"error": {
            "code": 404,
            "message": "Article not found!"
        }}, status=status.HTTP_404_NOT_FOUND)

    if request.method == "GET":
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    elif request.method == "PUT":
        serializer = ArticleSerializer(article, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == "DELETE":
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

api view에 해당되는 endpoint를 설정합니다.

#news/api/urls.py

from django.urls import path
from news.api.views import article_list_create_api_view, article_detail_api_view

urlpatterns = [
    path("articles/", article_list_create_api_view, name="article-list"),
    path("articles/<int:pk>/", article_detail_api_view, name="article-detail")
]


The APIView Class

위에서 작성한 FBV(function based view)를 APIView를 통해 class based view로 작성합니다.

#news/api/views.py

from rest_framework.views import APIView
from rest_framework.generics import get_object_or_404

class ArticleListCreateAPIView(APIView):

    def get(self, request):
        articles = Article.objects.filter(active=True)
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ArticleDetailAPIView(APIView):
    def get_object(self, pk):
        article = get_object_or_404(Article, pk=pk)
        return article

    def get(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        article = self.get_object(pk)
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
#news/api/urls.py

from django.urls import path
from news.api.views import ArticleListCreateAPIView, ArticleDetailAPIView

urlpatterns = [
    path("articles/", ArticleListCreateAPIView.as_view(), name="article-list"),
    path("articles/<int:pk>/", ArticleDetailAPIView.as_view(), name="article-detail")
]

Serializers Validation

is_valid() 부분을 customizing할 수 있습니다.
object level validaion이 있고, field level validation이 있습니다.

이를 serializers.py에 적용한 결과는 다음과 같습니다.

from rest_framework import serializers
from news.models import Article


class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    author = serializers.CharField()
    title = serializers.CharField()
    description = serializers.CharField()  # model에서 TextField인거
    body = serializers.CharField()
    location = serializers.CharField()
    publication_date = serializers.DateField()
    active = serializers.BooleanField()
    created_at = serializers.DateTimeField(read_only=True)
    updated_at = serializers.DateTimeField(read_only=True)

    def create(self, validated_data):
        print(validated_data)
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.author = validated_data.get('author', instance.author)
        instance.title = validated_data.get('title', instance.title)
        instance.description = validated_data.get(
            'description', instance.description)
        instance.body = validated_data.get('body', instance.body)
        instance.location = validated_data.get('location', instance.location)
        instance.publication_date = validated_data.get(
            'publication_date', instance.publication_date)
        instance.active = validated_data.get('active', instance.active)
        instance.save()
        return instance

    # object level validaion
    def validate(self, data):
        """ check that description and title are different """
        if data["title"] == data["description"]:
            raise serializers.ValidationError(
                "Title and Description must be different")
        return data

    # field level validation
    def validate_title(self, value):
        if len(value) < 60:
            raise serializers.ValidationError(
                "The title has to be at least 60 char")
        return value

The ModelSerializer Class

앞에서 작성했던 Serializer를 ModelSerializer를 통해 간단히 적을 수 있습니다. 또한, 새로운 필드를 정의해줄 수도 있습니다.

#news/api/serializers.py

from datetime import datetime
from django.utils.timesince import timesince
from rest_framework import serializers
from news.models import Article


class ArticleSerializer(serializers.ModelSerializer):
	
    #새로운 필드
    time_since_publication = serializers.SerializerMethodField()

    class Meta:
        model = Article
        exclude = ("id", )
        # fields = "__all__"  # we want all the fields of our model
        # fields=("title", "description", "body") #we want to choos a couple of fields

    def get_time_since_publication(self, object):
        publication_date = object.publication_date
        now = datetime.now()
        time_delta = timesince(publication_date, now)
        return time_delta

django shell을 통해 확인해보면 다음과 같습니다.

>>> from news.api.serializers import ArticleSerializer
>>> serializer = ArticleSerializer()
>>> print(repr(serializer))
ArticleSerializer():
    time_since_publication = SerializerMethodField()
    author = CharField(max_length=50)
    title = CharField(max_length=120)
    description = CharField(max_length=200)
    body = CharField(style={'base_template': 'textarea.html'})
    location = CharField(max_length=120)
    publication_date = DateField()
    active = BooleanField(required=False)
    created_at = DateTimeField(read_only=True)
    updated_at = DateTimeField(read_only=True)

앞서 했던 validation도 ModelSerializer에 그대로 사용할 수 있습니다.


How to handle Nested Relationships

먼저 추가적인 모델을 만들어서 Foreignkey로 연결합니다.

#news/api/models.py

from django.db import models


class Journalist(models.Model):
    first_name = models.CharField(max_length=60)
    last_name = models.CharField(max_length=60)
    biography = models.TextField(blank=True)

    def __str__(self):
        return f"{ self.first_name } { self.last_name }"


class Article(models.Model):
    author = models.ForeignKey(
        Journalist, on_delete=models.CASCADE, related_name="articles")
    title = models.CharField(max_length=120)
    description = models.CharField(max_length=200)
    body = models.TextField()
    location = models.CharField(max_length=120)
    publication_date = models.DateField()
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"{self.author} {self.title}"

이때, model이 바뀌었으므로, db를 초기화해야 하는데, 다음 명령어를 통해 db를 초기화하고 새로 migrate를 진행합니다.

docker-compose down --volume #db삭제
docker-compose up -d #재실행

docker-compose exec web python manage.py makemigrations
docker-compose exec web python manage.py migrate

그리고 이를 반영한 serializers.py는 다음과 같습니다.

from datetime import datetime
from django.utils.timesince import timesince
from rest_framework import serializers
from news.models import Article, Journalist


class ArticleSerializer(serializers.ModelSerializer):

    # 새로운 필드
    time_since_publication = serializers.SerializerMethodField()
    author = serializers.StringRelatedField() #string으로 처리
    # author = JournalistSerializer(read_only=True)

    class Meta:
        model = Article
        exclude = ("id", )
        # fields = "__all__"  # we want all the fields of our model
        # fields=("title", "description", "body") #we want to choos a couple of fields

    def get_time_since_publication(self, object):
        publication_date = object.publication_date
        now = datetime.now()
        time_delta = timesince(publication_date, now)
        return time_delta

    # object level validaion
    def validate(self, data):
        """ check that description and title are different """
        if data["title"] == data["description"]:
            raise serializers.ValidationError(
                "Title and Description must be different")
        return data

    # field level validation
    def validate_title(self, value):
        if len(value) < 60:
            raise serializers.ValidationError(
                "The title has to be at least 60 char")
        return value


class JournalistSerializer(serializers.ModelSerializer):
    articles = serializers.HyperlinkedRelatedField(
        many=True, read_only=True, view_name="article-detail")  # endpoint가 들어감
    #articles = ArticleSerializer(many=True, read_only=True)

    class Meta:
        model = Journalist
        fields = "__all__"

0개의 댓글

관련 채용 정보