본문 바로가기

회고록(TIL&WIL)

TIL 2022.06.24 DRF/ User관련 구현, Article관련 구현, 특강 숙제

User관련 구현

user/models.py

# user/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

# custom user model 사용 시 UserManager 클래스와 create_user, create_superuser 함수가 정의되어 있어야 함
class UserManager(BaseUserManager):
    def create_user(self, username, password=None):
        if not username:
            raise ValueError('Users must have an username')
        user = self.model(
            username=username,
        )
        user.set_password(password)
        user.save(using=self._db)
        return user
    
    # python manage.py createsuperuser 사용 시 해당 함수가 사용됨
    def create_superuser(self, username, password=None):
        user = self.create_user(
            username=username,
            password=password
        )
        user.is_admin = True
        user.save(using=self._db)
        return user

# 유저
class User(AbstractBaseUser):
    username = models.CharField("사용자계정", max_length=50, unique=True)
    password = models.CharField("비밀번호", max_length=128)
    email = models.EmailField("이메일", max_length=100)
    fullname = models.CharField("이름", max_length=20)
    join_date = models.DateTimeField("가입일", auto_now_add=True)

    is_active = models.BooleanField(default=True) # 계정활성화 여부
    is_admin = models.BooleanField(default=False) # 관리자 계정 여부

    USERNAME_FIELD = 'username' # 로그인 시 사용할 필드 지정
    REQUIRED_FIELDS = [] # createsuperuser 할 때 추가로 요구할 필드 지정
    objects = UserManager() # custom user 생성 시 필요

    def __str__(self):
        return self.username

    # 로그인 사용자의 특정 테이블의 crud 권한을 설정, perm table의 crud 권한이 들어간다.
    # admin일 경우 항상 True, 비활성 사용자(is_active=False)의 경우 항상 False
    def has_perm(self, perm, obj=None):
        return True
    
    # 로그인 사용자의 특정 app에 접근 가능 여부를 설정, app_label에는 app 이름이 들어간다.
    # admin일 경우 항상 True, 비활성 사용자(is_active=False)의 경우 항상 False
    def has_module_perms(self, app_label): 
        return True
    
    # admin 권한 설정
    @property
    def is_staff(self): 
        return self.is_admin
        
# 유저 프로필 - 취미
class Hobbies(models.Model):
    name = models.CharField("취미", max_length=20)
    def __str__(self):
        return self.name

# 유저 프로필
class UserProfile(models.Model):
    user = models.OneToOneField(User, verbose_name="유저",on_delete=models.CASCADE)
    discription = models.TextField("자기소개", null=True, blank=True)
    hobby = models.ManyToManyField(Hobbies, verbose_name="취미")
    birthday = models.DateField("생일")

    def __str__(self):
        return self.user.username

url connection

# projectOOO/url.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('user.urls')),
]
# user/urls.py
from django.urls import path
from user import views

urlpatterns = [
    # user/
    path('', views.UserAPIView.as_view()),
    path('sign', views.UserSignAPIView.as_view()),
]

user/view.py (로그인/로그아웃/회원가입/수정/탈퇴)

from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import permissions
from django.contrib.auth import login, authenticate, logout
from rest_framework import status

from .serializers import UserSerializer
from .models import User as UserModel

# Create your views here.

class UserAPIView(APIView):
    permission_classes = [permissions.AllowAny]
    
    # 요청을 보낼 method의 이름으로 함수명을 지어 오버라이딩 해서 사용해야함
    # 회원 정보 출력
    def get(self, request):
        user = request.user
        #serializer에 queryset을 인자로 줄 경우(ManyToMany관계일 때) many=True 옵션을 줘야한다.
        serialized_user_data = UserSerializer(user).data
        return Response(serialized_user_data, status=status.HTTP_200_OK)

    # 로그인
    def post(self, request):
        #username = request.data.get('username', '')
        #password = request.data.get('password', '')
        #user = authenticate(request, username=username, password=password)
        user = authenticate(request, **request.data)

        if not user:
            return Response({'error': '아이디와 패스워드를 확인해주세요!'})
        login(request, user)
        return Response({'success': '로그인 성공!'}, status=status.HTTP_200_OK)
        
    # 로그아웃
    def delete(self, request):
        logout(request)
        return Response({'success': '로그아웃 성공!'}, status=status.HTTP_200_OK)


class UserSignAPIView(APIView):
    # 회원 가입
    def post(self, request):
        userSerializer = UserSerializer(data=request.data)
        userSerializer.is_valid(raise_exception=True)
        userSerializer.save()
        return Response(userSerializer.data, status=status.HTTP_200_OK)

    # 회원 수정
    def put(self, request):
        userSerializer = UserSerializer(request.user, data=request.data, partial=True)
        userSerializer.is_valid(raise_exception=True)
        userSerializer.save()
        return Response(userSerializer.data, status=status.HTTP_200_OK)

    # 회원 탈퇴
    def delete(self, request):
        UserModel.objects.get(id=request.user.id).delete()
        return Response({"success": "회원탈퇴 성공!"}, status=status.HTTP_200_OK)

user/serializers.py

from rest_framework import serializers

from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobbies as HobbiesModel


class HobbySerializer(serializers.ModelSerializer):
    # 같은 취미를 가진 유저를 함께 출력
    same_hobby_users = serializers.SerializerMethodField()
    def get_same_hobby_users(self, obj):
        user_list = []
        # obj => HobbiesModel 이를 이용해서 userprofile을 역참조
        for user_profile in obj.userprofile_set.all():
            user_list.append(user_profile.user.username)

        return user_list

    class Meta:
        model = HobbiesModel
        fields = ["name", "same_hobby_users"]

class UserProfileSerializer(serializers.ModelSerializer):
    hobby = HobbySerializer(many=True)
    class Meta:
        model = UserProfileModel
        fields = ["discription", "birthday", "hobby"]

class UserSerializer(serializers.ModelSerializer):
    def create(self, validated_data):
        # 패스워드 암호화를 위해서 패스워드만 분리
        password = validated_data.pop("password", "")
        # 유저 모델에 나머지 데이터 세팅
        user = UserModel(**validated_data)
        # 패스워드 암호화하여 유저모델에 세팅
        user.set_password(password)
        # 유저 정보 DB에 저장
        user.save()
        return user

    userprofile = UserProfileSerializer()
    class Meta:
        # serializer에 사용될 model, field지정
        model = UserModel
        # 모든 필드를 사용하고 싶을 경우 fields = "__all__"로 사용
        fields = ["username", "fullname", "email", "userprofile", ]

Article관련 구현

article/models.py

from datetime import timedelta
from datetime import datetime
from django.db import models
from user.models import User

# 카테고리
class Category(models.Model):
    name = models.CharField("카테고리명",max_length=20)
    discription = models.TextField("설명")
    def __str__(self):
        return self.name


class Article(models.Model):
    #글 작성자, 글 제목, 카테고리, 글 내용
    user = models.ForeignKey(User, verbose_name="작성자", on_delete=models.CASCADE)
    title = models.CharField("제목", max_length=50)
    category = models.ManyToManyField(Category, verbose_name="카테고리")
    content = models.TextField("글내용")
    start_date = models.DateTimeField("시작날짜", auto_now_add=True)
    end_date = models.DateTimeField("끝날짜", default=datetime.now() + timedelta(days=7) )
    def __str__(self):
        return self.title
        
# 댓글
class Comment(models.Model):
    user = models.ForeignKey(User, verbose_name="작성자", on_delete=models.CASCADE)
    article = models.ForeignKey(Article, verbose_name="게시글", on_delete=models.CASCADE)
    comment = models.TextField("댓글내용")
    def __str__(self):
        return (f"{self.user} / {self.article} / {self.comment}")

article/views.py

from rest_framework.response import Response
from rest_framework.views import APIView
from DRF_study.permissions import RegistedMoreThan3MinsUser
from rest_framework import status
from .models import Article as ArticleModel

from DRF_study.serializers import ArticleSerializer


class ArticleView(APIView):
    # 가입한지 3분지난 유저들만 글을 쓸 수 있도록 하는 permission클래스
    permission_classes = [RegistedMoreThan3MinsUser]
    # 전체 글 조회
    def get(self, request):
        return Response(ArticleSerializer(ArticleModel.objects.all(), many=True).data, status=status.HTTP_200_OK)

    # 글 쓰기
    def post(self, request):
        user = request.user
        request.data["user"] = user.id
        article_serializer = ArticleSerializer(data=request.data)
        article_serializer.is_valid(raise_exception=True)
        article_serializer.save()

        return Response(article_serializer.data, status=status.HTTP_200_OK)

article/serializers.py

from rest_framework import serializers

from blog.models import Comment as CommentModel
from blog.models import Category as CategoryModel
from blog.models import Aricle as ArticleModel

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = CommentModel
        fields = ["user", "article", "comment"]

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = CategoryModel
        fields = ["name", "discription"]

class ArticleSerializer(serializers.ModelSerializer):
    # 하나의 게시글의 여러개의 댓글이므로 역참조를 이용하기
    comment = CommentSerializer(many=True, read_only=True, source="comment_set")
    # 카테고리는 id 값이아닌 카테고리명으로 출력되도록
    def get_category(self, obj):
         return obj.category.name
    class Meta:
        model = ArticleModel
        fields = ["user", "title", "category", "content", "comment"]
        read_only_fields = ["category"]

특강 숙제 Product 관련 구현

product/models.py

from django.db import models
from user.models import User as UserModel

# 상품 <작성자, 썸네일, 상품 설명, 등록일자, 노출 종료 일자, 가격, 수정 일자, 활성화 여부>
class Product(models.Model):
    author = models.ForeignKey(UserModel, verbose_name="작성자", on_delete=models.CASCADE)
    title = models.CharField("제목", max_length=50, blank=False, null=False)
    thumnail = models.FileField("이미지", upload_to="product/")
    discription = models.TextField("설명")
    add_date = models.DateTimeField("등록일자", auto_now_add=True)
    exposure_end_date = models.DateTimeField("끝일자")
    price = models.IntegerField("가격")
    update_data = models.DateTimeField("수정일자", auto_now=True)
    is_active = models.BooleanField("활성화여부", default=True)

# 리뷰 <작성자, 상품, 내용, 평점, 작성일>
class Review(models.Model):
    author = models.ForeignKey(UserModel,verbose_name="작성자", on_delete=models.SET_NULL, null=True)
    product = models.ForeignKey(Product, verbose_name="상품", on_delete=models.SET_NULL, null=True)
    content = models.TextField("내용", default="")
    created = models.DateTimeField("작성일", auto_now_add=True)
    rating = models.IntegerField("평점")

product/serializers.py

from django.utils import timezone
from rest_framework import serializers

from django.db.models import Avg

from product.models import Product as ProductModel
from product.models import Review as ReviewModel

class ReviewSerializer(serializers.ModelSerializer):
    author = serializers.SerializerMethodField()
    def get_author(self, obj):
        return obj.author.fullname

    class Meta:
        model = ReviewModel
        fields = ["author", "product", "content", "created", "rating", ]

class ProductInfoSerializer(serializers.ModelSerializer):
    # 상품 정보를 리턴 할 때 상품에 달린 review와 평균 점수를 함께 리턴해주세요
    review = serializers.SerializerMethodField()
    def get_review(self, obj):
        # 해당 게시물에 달린 리뷰들을 전부 다 가져옴
        reviews = obj.review_set
        # 작성 된 리뷰는 모두 return하는 것이 아닌, 가장 최근 리뷰 1개만 리턴해주세요
        return {
            "last_review": ReviewSerializer(reviews.last()).data,
            "average_rating": reviews.aggregate(avg=Avg("rating"))["avg"]
        }

    author = serializers.SerializerMethodField()
    def get_author(self, obj):
        return obj.author.username

    class Meta:
        model = ProductModel
        fields = ["author", "title", "thumnail",
                  "description", "created", 
                  "exposure_end_date", "review" ]


class ProductSerializer(serializers.ModelSerializer):
    # 노출 종료 일자가 현재보다 더 이전 시점이라면 상품을 등록할 수 없도록
    def validate(self, data):
        end_date = data.get("exposure_end_date", "")
        # end_date 가 DatetiemField 이기 때문에! django.utils 의 timezone을 써야함!
        # end_date 잇고! 현재보다 end_date가 과거 일 경우 error 출력
        if  end_date and timezone.now() > end_date:
            raise serializers.ValidationError(
                detail= {"error": "노출 일자가 종료되었습니다."}
            )
        return data

    def create(self, validated_data):
        #상품 설명의 마지막에 "<등록 일자>에 등록된 상품입니다." 라는 문구를 추가해주세요
        # 우선 검증이 끝난 데이터들을 Model에 다 맞게 담고
        product = ProductModel(**validated_data)
        # 세이브를 먼저 한번해야 product.created가 생기게 하기 위함
        product.save()
        # product.created를 원하는 형식으로 조정한뒤 다시 save()
        product.description += f"\n\n{product.created.replace(microsecond=0, tzinfo=None)}에 등록된 상품입니다."
        product.save()

        return product       

    def update(self, instance, validated_data):
        # 수정하였을 때 상품 설명 마지막에 <등록일자>에 등록된 상품이 수정되도록
        # dict 형태인 validated_data를 풀어서 쓰기 위해 items()함수로 for문 돌리기
        for key, value in validated_data.items():
            # 상품설명만 바꿔야되기 때문에 key 가 description 인 경우에만한에서
            if key == "description":
                # value에 created 그대로 집어넣기
                value += f"\n\n{instance.created.replace(microsecond=0, tzinfo=None)}에 등록된 상품입니다."
            setattr(instance, key, value)
        instance.save() # save한번해야지 modified 값을 가져올 수 있다

        instance.description = f"{instance.modified.replace(microsecond=0, tzinfo=None)}에 수정 되었습니다. \n" + instance.description
        instance.save()                             
        return instance

    class Meta:
        model = ProductModel
        fields = ["author", "title", "thumnail",
                  "description", "created", "price",
                  "exposure_end_date", "modified", ]

product/views.py

from datetime import datetime
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from rest_framework import permissions
from django.db.models import Q

from product.serializers import ProductSerializer
from product.serializers import ProductInfoSerializer
from product.models import Product as ProductModel

class ProductAPIView(APIView):
    permission_classes = [RegistedMoreThan3MinsUser]

    # 상품 목록 출력
    def get(self, request):
        today = datetime.now()
        # 오늘기준으로 노출시작날짜가 이전, 노출종료날짜가 이후 or 로그인된 유저거나
        products = ProductModel.objects.filter(
            Q(exposure_start_date__lte=today, exposure_end_date__gte=today) 
            | Q(author=request.user)
        )
        # serializer에 queryset을 주기 때문에 many=True를 줘야함
        serialized_data = ProductInfoSerializer(products, many=True).data

        return Response(serialized_data, status.HTTP_200_OK)

    # 상품 등록
    def post(self, request):
        # 현재 접속중인 유저의 데이터의 id request.data의 author에 저장
        request.data["author"] = request.user.id
        product_serializer = ProductSerializer(data=request.data)
        
        if product_serializer.is_valid():
            product_serializer.save()
            return Response(product_serializer.data, status=status.HTTP_200_OK)
        
        return Response(product_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    # 상품 수정
    def put(self, request, product_id):
        # product/<product_id> 헤더에서 id값을 받아오고 그 상품의 object를 get으로 얻어옴
        try:
            product = ProductModel.objects.get(id=product_id)
        # 해당 상품이 없을 수 도 있기 때문에 try except 문 사용
        except ProductModel.DoesNotExist:
            return Response({"error" : "없는 상품 입니다."},
                                    status=status.HTTP_400_BAD_REQUEST)

        # request.data['user'].pop() #바꾸고 싶지 않은 데이터는 제외하기
        product_serlizer = ProductSerializer(product, data=request.data, partial=True)
        product_serlizer.is_valid(raise_exception=True)
        product_serlizer.save()
        return Response(product_serlizer.data, status=status.HTTP_200_OK)

permissions.py

# DRF_study/permissions.py
from rest_framework.permissions import BasePermission
from datetime import timedelta
from django.utils import timezone
from rest_framework.exceptions import APIException
from rest_framework import status

# BasePermission 상속 필수!
class RegistedMoreThan3MinsUser(BasePermission):
    # False 리턴 시 보여주는 메세지
    message = '가입 후 3분 이상 지난 사용자만 사용하실 수 있습니다.'
    SAFE_METHODS = ('GET', )

    # has_permission 오버라이딩
    def has_permission(self, request, view):
        user = request.user
        # 로그인한 이용자만 사용 가능
        if not user.is_authenticated:
            response ={
                    "detail": "서비스를 이용하기 위해 로그인 해주세요.",
                }
            raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)
        
        # 로그인한 사용자가 get 요청 시(self.SAFE_METHODS)
        # if request.method == "GET":
        if user.is_authenticeted and request.method in self.SAFE_METHODS:
            return True

        # 그 외의 방식으로 요청 시
        # 가입 후 3분이 지난 사용자만 허가 ex) 글쓰기 권한
        return bool(user.is_authenticated and 
                    request.user.join_date < (timezone.now() - timedelta(minutes=3)))
                

                
# is_authenticated 값이 없을 경우 발생 시킬 예외 정의
class GenericAPIException(APIException):
    def __init__(self, status_code, detail=None, code=None):
        self.status_code=status_code
        super().__init__(detail=detail, code=code)

class IsAdminOrIsAuthenticatedReadOnly(BasePermission):
    # 메서드 지정을 통해서도 세부적인 권한 조절 가능
    SAFE_METHODS = ('GET', )
    # return 값 False 일 때 전달할 메세지
    message = '접근 권한이 없습니다.'

    def has_permission(self, request, view):
        user = request.user
        # 먼저 로그인 되어있지 않은 것을 판별해서 예외 처리해야만한다 하지않을 경우
        # 로그인되어있지 않다는 오류가 메세지로 나오고 접근 권한에 대해서는 판별도 되지 않게 됨
        if not user.is_authenticated:
            response ={
                    "detail": "서비스를 이용하기 위해 로그인 해주세요.",
                }
            raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)
        # 로그인 ok, admin ok
        if user.is_authenticated and user.is_admin:
            return True
        # 로그인 ok, method==GET ok, 
        elif user.is_authenticated and request.method in self.SAFE_METHODS:
            return True
        
        return False