내가 맡은 역할
우선 erd 모델에 맞게 장고에 모델을 구축해주었다.
book/models.py
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
# Create your models here.
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self) -> str:
return self.name
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Categories" # 없으면 admin 페이지에 Categorys 라고 표시됨
class Book(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
cover = models.ImageField(blank=True, upload_to="media/bookCover")
author = models.ForeignKey(Author, null=True, on_delete=models.SET_NULL)
category = models.ForeignKey(
Category, blank=True, null=True, on_delete=models.SET_NULL
)
intro = models.TextField()
tags = TaggableManager()
likes = models.ManyToManyField("user.User", related_name="liked_books", blank=True)
def __str__(self) -> str:
return self.title
num_choices = zip(range(0, 6), range(0, 6))
class Review(models.Model):
title = models.CharField(max_length=250, default="")
author = models.ForeignKey("user.User", on_delete=models.CASCADE)
book = models.ForeignKey(
Book, on_delete=models.CASCADE
) # 역참조이므로 related_at 을 써주지 않아도 review_set 이 디폴트로 있음
content = models.TextField()
star = models.IntegerField(blank=True, null=True, choices=num_choices)
likes = models.ManyToManyField(
"user.User", related_name="liked_reviews", blank=True
)
created_at = models.DateField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
카테고리와 작가는 해당 책이 없어도 별개로 존재하므로 책과 별개의 엔티티로 만들어주었다. 처음에 카테고리-책 은 M : N 으로 설정할까 1 : N 으로 설정할까 고민을 많이 했는데, 내가 자주 사용하는 서점 사이트인 리디북스를 보면 카테고리를 세부적으로 설정하기 보다는 주장르로 카테고리를 하나 설정해두고 태그로 세부 장르를 표시해두는 것 같아 그 방식을 채택했다.
user/models.py
from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
from book.models import Author
class UserManager(BaseUserManager):
def create_user(self, email, username, nickname, birthday, profile, password=None):
"""
Creates and saves a User with the given email, date of
birth and password.
"""
if not email:
raise ValueError("Users must have email")
user = self.model(
email=self.normalize_email(email),
username=username,
nickname=nickname,
birthday=birthday,
profile=profile,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, nickname, email, birthday, password=None):
"""
Creates and saves a superuser with the given email, date of
birth and password.
"""
user = self.create_user(
email,
username,
nickname,
birthday,
password=password,
)
user.is_admin = True
user.save(using=self._db)
return user
class User(AbstractBaseUser):
email = models.EmailField(
verbose_name="email address",
max_length=255,
unique=True,
)
username = models.CharField(max_length=100)
nickname = models.CharField(max_length=100)
profile = models.ImageField(blank=True, upload_to="media/userProfile")
birthday = models.DateField(blank=True)
join_date = models.DateField(auto_now_add=True)
follower = models.ManyToManyField(
Author, symmetrical=False, related_name="followee", blank=True
)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username", "nickname", "birthday"]
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
"Does the user have a specific permission?"
# Simplest possible answer: Yes, always
return True
def has_module_perms(self, app_label):
"Does the user have permissions to view the app `app_label`?"
# Simplest possible answer: Yes, always
return True
@property
def is_staff(self):
"Is the user a member of staff?"
# Simplest possible answer: All admins are staff
return self.is_admin
유저모델은 설정해둔 필드에 맞게 커스터마이징을 조금 해줬다. 이미지필드를 추가해줬으므로 admin.py 도 수정했다.
user/admin.py
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from .models import User
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
password2 = forms.CharField(
label="Password confirmation", widget=forms.PasswordInput
)
profile = forms.ImageField(label="Profile Picture", required=False)
class Meta:
model = User
fields = ["email", "username", "nickname", "birthday", "profile"]
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
disabled password hash display field.
"""
password = ReadOnlyPasswordHashField()
class Meta:
model = User
fields = [
"email",
"password",
"username",
"nickname",
"birthday",
"profile",
"is_active",
"is_admin",
]
class UserAdmin(BaseUserAdmin):
# The forms to add and change user instances
form = UserChangeForm
add_form = UserCreationForm
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ["username", "email", "nickname", "is_admin"]
list_filter = ["is_admin"]
fieldsets = [
(
None,
{
"fields": [
"username",
"nickname",
"email",
"password",
"follower",
],
},
),
("Personal info", {"fields": ["birthday"]}),
("Permissions", {"fields": ["is_admin"]}),
]
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = [
(
None,
{
"classes": ["wide"],
"fields": [
"email",
"username",
"nickname",
"profile",
"birthday",
"follower",
"password1",
"password2",
],
},
),
]
search_fields = ["email"]
ordering = ["email"]
filter_horizontal = []
# Now register the new UserAdmin...
admin.site.register(User, UserAdmin)
# ... and, since we're not using Django's built-in permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)
이미지 필드인 profile 을 받는 폼을 따로 만들어준 뒤 add_fieldsets 에 profile 을 추가했다.
설정해둔 API 에 맞춰 urls.py 에 url 과 API 클래스들을 미리 써뒀다.
user/urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from django.urls import path
urlpatterns = [
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
book/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("mainpage/", views.Main().as_view(), name="mainpage"),
path("book/<int:book_id>/", views.BookDetail.as_view(), name="detail-book"),
path(
"book/<int:book_id>/review/", views.ReviewCreate.as_view(), name="review-create"
),
path(
"book/<int:book_id>/review/<int:review_id>/",
views.ReviewUpdate.as_view(),
name="review-update",
),
]
이제 views.py 에 클래스를 미리 설정해두면 기본 세팅은 끝이다.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404
from rest_framework import status, permissions
from .models import Book, Review, Author, TaggableManager
from .serializers import (
BookSerializer,
ReviewSerializer,
BookTagSerializer,
ReviewCreateSerializer,
)
from taggit.serializers import TagListSerializerField, TaggitSerializer
from django.views.generic import TemplateView, ListView
from taggit.models import Tag
# Create your views here.
class Main(APIView):
def get(self, request):
# 추천 책
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
# 인기 리뷰
reviews = Book.objects.all().order_by("likes").values()
re_serializer = ReviewSerializer(reviews, many=True)
# 태그
tags = Tag.objects.all()
tag_serializer = TaggitSerializer(tags)
return Response(serializer.data, status=status.HTTP_200_OK)
class BookDetail(APIView):
def get(self, request, book_id):
book = get_object_or_404(id=book_id)
serializer = BookTagSerializer(book)
return Response(serializer.data, status=status.HTTP_200_OK)
class ReviewCreate(APIView):
def post(self, request, book_id):
serializer = ReviewCreateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user, book_id=book_id)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ReviewUpdate(APIView):
def put(self, request, book_id, review_id):
pass
def delete(self, request, book_id, review_id):
pass
# html로 태그 전달
class TagCloudTV(TemplateView):
# 모든 태그 추출
template_name = "taggit/tag_cloud_view.html"
class TaggedObjectLV(ListView):
# 특정 태그가 있는 책들
template_name = "taggit/tag_with_book.html"
model = Book
def get_queryset(self):
return Book.objects.filter(tags__name=self.kwargs.get("tags"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["tags"] = self.kwargs["tags"]
return context