본문 바로가기

매일 TIL

[내일배움캠프 8-7일] 댓글 구현하기, Custom User Model, 1:N 확장

댓글 구현

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    content = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.content

articles 앱의 models.py에 Comment 클래스를 추가해준다.

on_delete 파라미터는 "만약 1번 Article이 삭제되면 거기 달려있는 코멘트는 어떻게 처리할까?" 에 대한 것.

즉, 참조하는 객체를 어떻게 처리할지에 대한 부분이다. 공식문서 참고하자.

하던대로 migrate까지 해준다.

 

한번 댓글을 써보자

view 구현 전에 shell_plus로 해보자.

이렇게도 댓글을 추가할 수 있다.

Comment.objects.all()로 조회해보면 두개 잘 들어간 걸 확인할 수 있음.

만약 이 댓글이 달린 article의 제목을 조회하고 싶다면?

join 따위 필요없음. comment.article.title로 바로 조회할 수 있다.

반대로 한 article의 모든 댓글을 조회하고 싶다면?

article.comment_set.all()로 해주면 된다.

*역참조를 할 때는 _set 이라는 매니저가 붙어주면 된다.

 

근데 _set이라니 너무 뜬금없는데?

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
    content = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.content

ForeignKey에 related_name='comments'를 추가해주면 끝.

이러면 comment_set가 아닌, comments로 사용할 수 있다는 것.

참고로 다시 migrate 단계를 해줘야 한다.(생략)

이제는 article.comments.all()로 잘 조회된는 걸 확인할 수 있다.


댓글 생성 구현

이제 페이지에서 댓글을 쓸 수 있도록 구현해보자.

path('<int:pk>/comments/', views.comment_create, name='comment_create'),

articles 앱의 urls.py에 경로 추가.

from .forms import ArticleForm, CommentForm
@require_POST
def comment_create(request, pk):
    form = CommentForm(request.POST)
    if form.is_valid():
        form.save()
        return redirect("articles:article_detail", pk)

articles앱의 views.py

comment_create 뷰를 생성.

comment는 GET 요청이 필요 없음.

GET은 보여주는 요청인데, 댓글은 article 디테일 페이지를 GET 할 때 포함되어 바로 보여지기 때문에.

즉 GET, POST 조건문으로 나누는 과정이 필요 없다는 것.

from .models import Article, Comment
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = "__all__"

articles 앱의 forms.py

view에서 가져올 CommentForm을 만들어 준다.

당연히 우리가 만들었던 Comment 모델을 import 해줘야 함.

def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    comment_form = CommentForm()
    context = {
        'article': article,
        'comment_form': comment_form,
        }
    return render(request, 'articles/article_detail.html', context)

다시 views.py

디테일 페이지에서 댓글을 작성할 수 있도록 디테일 뷰에 comment_form = CommentForm() 문장을 추가.

context에도 추가해준다.

<br>
<hr>
<h3>댓글</h3>
<form action="{% url 'articles:comment_create' article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form.as_p }}
    <input type="submit" value="작성">
</form>

article_detail.html

하던대로 하자. 폼을 추가해주면 된다.

 

그런데 댓글 작성란을 보면 article을 내가 선택할 수 있도록 되어 있음. 그 페이지로 고정해야 한다.

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = "__all__"
        exclude = ('article',)

forms.py에서 exclude를 통해 제외시켜주면 끝.

보다시피 댓글창이 잘 구현되는 것을 볼 수 있음.

 

여기서 문제가 또 발생, 댓글 입력 후 버튼을 누르면 터져버린다.

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
    content = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.content

models.py

우리가 사용하는 CommentForm 참조하는 모델인 Comment를 확인해보면 article 값이 반드시 필요하다.

그런데 우리는 댓글창에 content 밖에 입력할 수 없으므로 넘어오는 article은 존재하지 않는 것.

그러면 article을 어떻게 입력해야하지?

@require_POST
def comment_create(request, pk):
    article = get_object_or_404(Article, pk=pk)
    form = CommentForm(request.POST)
    if form.is_valid():
        comment = form.save(commit=False)
        comment.article = article
        comment.save()
    return redirect("articles:article_detail", article.pk)

views.py를 수정해준다.

생성된 comment 인스턴스를, commit=False를 통해 DB에 저장하지 않은 상태로 두는 것.

저장하기 전에 comment.article = article로 article을 넣어주고, 이후 저장하면 된다.

이렇게 하면 댓글 작성까지는 완료.

 

이제 페이지에 댓글을 보여줘야 하지 않을까?

def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    comment_form = CommentForm()
    comments = article.comments.all()
    context = {
        'article': article,
        'comment_form': comment_form,
        'comments': comments
        }
    return render(request, 'articles/article_detail.html', context)

views.py의 페이지 디테일 뷰를 수정해주면 된다.

comments 변수에 article.comments.all()을 통해 모든 댓글을 넣어두고,

역시 context에 추가해주면 됨.

<br>
<hr>
<h3>댓글</h3>
<form action="{% url 'articles:comment_create' article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form.as_p }}
    <input type="submit" value="작성">
</form>

<ul>
    {% for comment in comments %}
        <li>
            <p>{{ comment.content }}</p>
        </li>
    {% endfor %}
</ul>

이후 article_detail.html에서 보여주기만 하면 됨.

반복문 템플릿 태그를 활용해 모든 댓글을 보여주면 끝.

 

최근 댓글이 위에 오도록 해보자.

def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    comment_form = CommentForm()
    comments = article.comments.all().order_by('-pk')
    context = {
        'article': article,
        'comment_form': comment_form,
        'comments': comments
        }
    return render(request, 'articles/article_detail.html', context)

views.py에서 디테일 뷰에 order_by('-pk')를 추가해주면 끝.


Custom User Model

우선 AUTH_USER_MODEL은 프로젝트 생성 후 바로 설정해준 뒤 마이그레이션까지 해주는 것이 일반적이다.

우리는 배우는 과정이라 지금 하는 것. 안그러면 나중에 수정 엄청해야함.

우선적으로 "나 커스텀 모델 사용할거다." 라고 알려줘야 한다는 것.

from django.db import models
from django.contrib.auth.models import AbstractUser


# Create your models here.
class User(AbstractUser):
    pass

accounts 앱의 models.py

우선은 이렇게 틀을 잡아만 놔도 의미가 있다.

장고에게 "기본 유저모델이 아닌 내가 만든 커스텀 유저 모델을 사용해라." 라는 의미를 부여하는 것.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

AUTH_USER_MODEL = 'accounts.User'

# Password validation

AUTH_PASSWORD_VALIDATORS = [

settings.py에 AUTH_USER_MODEL = 'accounts.User' 를 추가해준다.

"accounts앱의 User라는 클래스를 나는 이 프로젝트에서 AUTH_USER_MODEL로 사용할거야." 라는 뜻.

추가하는 위치는 튜터님이 쓰는 방식이고 사실상 어디든 상관없음.

위에서도 말했듯이 프로젝트 만들고 여기까지 일단 한 뒤에 Migrate.

이후 프로젝트를 진행하면 된다.


DB 날리기

우선 우리 데이터가 들어있는 db.sqlite3 삭제.

migrations를 모두 날려야 하는데 우리는 articles에만 migrations가 존재.

articles 앱의 migration을 모두 삭제.(0001~0005까지)

이후 makemigrations, migrate 해준다.

DB를 확인해보면 아무 데이터도 남아있지 않은 것을 볼 수 있다.

일단 admin으로 superuser하나 만들자.

 

이후 회원가입을 해보면 오류가 나는 것을 확인할 수 있다.

아직 장고 내부의 auth 안에 있는 User를 사용하고 있기 때문.

우리는 아까 커스텀한 accounts앱 내부의 User 클래스를 사용해야 한다.

from django.contrib.auth.forms import UserChangeForm, UserCreationForm
class CustomUserCreationForm(UserCreationForm):
    class Meta:
        model = get_user_model()
        fields = UserCreationForm.Meta.fields + (필요시 추가필드)

accounts앱의 forms.py에서 UserCreationForm을 상속받는 CustomUserCreationForm을 생성한다.

get_user_model()을 통해 참조받는 것.

from .forms import CustomUserChangeForm, CustomUserCreationForm
@require_http_methods(['GET', 'POST'])
def signup(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('index')
    else:
        form = CustomUserCreationForm()
    context = {'form': form}
    return render(request, 'accounts/signup.html', context)

views.py도 바꿔준다. UserCreationForm이 아닌 CustomUserCreationForm으로.

물론 import도 해줘야 함.

이제는 회원가입이 잘 되는 것을 확인할 수 있다.

 

우리가 Custom User를 바꿔주면 admin도 바꿔주어야 한다.

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

admin.site.register(User, UserAdmin)

accounts 앱의 admin.py 수정

우리 기본 User모델에 잘 등록된 것을 볼 수 있다.

 

정리하자면

프로젝트를 생성한 뒤 AUTH_USER_MODEL을 바로 설정해주고, 마이그레이션까지 해주는 것이 일반적이다.
만약 그러지 않았다면 DB를 모두 삭제, migrations까지 전부 삭제 후 위 과정을 진행한다.

 


1:N 확장하기

이제는 article에 각각의 작성자를 넣어보자.

from django.conf import settings
class Article(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    image = models.ImageField(upload_to='images/', blank=True)

    author = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="articles"
    )
   
    def __str__(self):
        return self.title

articles앱의 models.py

우선 settings를 import해주고, Article 클래스에 author = models.ForeignKey() 부분을 추가해 주면 된다.

settings에서 정해둔 AUTH_USER_MODEL을 사용하겠다는 뜻.

역참조할때 _set을 사용하지 않기 위해 related_name도 설정해 준다.

이후 역시 migration 만든다.

여기서 메시지가 나오는데 1번 옵션으로 진행하면 됨.

migrate까지 해주면 끝.

 

문제 발생, 작성자를 선택하는 창이 나옴. 로그인 된 유저가 작성자가 되도록 고정하자.

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = '__all__'
        exclude = ('author',)

articles앱의 forms.py

이젠 익숙함. exclude로 빼주면 된다.

 

터짐, 그냥 또 터진다.

우리 모델, 즉 Article 클래스에서는 author를 꼭 필요로 함.

하지만 author가 POST되는 데이터 안에 없음.

이래서 터지는구나. 아까 했던거 commit 쓰는 그거구나 하면 됨.

@login_required
def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST, request.FILES)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
            return redirect('articles:article_detail', article.pk)
    else:
        form = ArticleForm()

    context = {'form': form}
    return render(request, 'articles/create.html', context)

역시 commit을 False로 해주고,

article.author에는 지금 로그인한 유저를 넣어주면 된다.

이후 세이브까지 하면 완료.

<h2>글 상세 페이지</h2>
<p>제목: {{ article.title }}</p>
<p>작성자: {{ article.author.username }}</p>

article_detail.html

이제 디테일 페이지에서 제목 아래에 작성자를 보여주자.


오늘의 회고

내일 아침에 마저 하자..

 

내일의 목표는 강의 다듣기