[Django] 외래키와 댓글, 개인인증 커스터마이징

작성:    

업데이트:

카테고리:

태그: ,

Foreign Key

정의

  • 외래 키(외부 키)
  • 관계형 DB에서 한 테이블의 필드 중 다른 테이블의 행을 식별할 수 있는 키
  • 참조하는 테이블에서 속성(필드, 세로)에 해당하고, 이는 참조하는 테이블의 기본 키(PK)를 가리킴
  • 참조하는 테이블의 외래 키는 참조되는 태이블 행 1개에 대응
  • 참조하는 테이블에서 참조되는 테이블의 존재하지 않는 행을 참조 불가능
  • 참조하는 테이블의 행 여러 개참조되는 테이블의 동일한 행을 참조 가능

  • 1:N 관계에서 외래키는 N의 테이블이 가지고 있다.
    • 1 : 참조되는 테이블(게시물)
    • N : 참조하는 테이블(댓글)


특징

  • 키를 사용하여 부모 테이블의 유일한 값을 참조(참조 무결성)
  • 참조 무결성 : DB 관계 모델에서 관련된 2개의 테이블 간의 일관성
  • 외래 키의 값이 반드시 부모 테이블의 PK일 필요는 없지만 유일한 값이어야 함


ForeignKey field

  • CharField, TextField 등과 달리 ‘-Field’ 없이 ForeignKey 그 자체가 Field명
  • 2개의 위치인자 반드시 필요
    1. 참조하는 model class
    2. on delete 옵션
  • migrate 작업 시 필드 이름에 _id를 추가하여 DB 열 이름 생성
models.ForeignKey('self', on_delete=models.CASCADE)


comment 모델 정의

# articles/models.py

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE) # 소문자 단수형
    content = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.article
  • migration 하면 필드 이름에 _id를 붙여 FK 필드 추가
  • 필드명을 article로 하였으므로 articles_comment 테이블article_id로 생성
  • 소문자 단수형으로 쓴 이유
    • 누구를 참조하는지
    • 다른 모델 관계와 헷갈리지 않을 수 있음(N:M 관계와 구분)


ForeignKey arguments : on_delete

  • 외래 키를 참조하는 객체가 사라졌을 때, 외래 키를 가진 객체를 어떻게 처리할 것인지 정의
  • DB Integrity(데이터 무결성)을 위해서 매우 중요
  • on_delete 옵션에 사용 가능한 값들
    • CASCADE ⭐ : 부모 객체(참조된 객체)와 함께 삭제
    • PROTECT
    • SET_NULL
    • SET_DEFAULT
    • SET()
    • DO_NOTHING
    • RESTRICT


데이터 무결성

  • 데이터의 정확성과 일관성을 유지하고 보증


개체 무결성(Entity integrity)

  • PK의 개념과 관련
  • 모든 테이블이 PK를 가지고, PK로 선택된 열은 고유한 값
  • PK는 빈 값을 허용 X


참조 무결성(Referential integrity) ⭐

  • FK의 개념과 관련
  • FK 값이 DB의 특정 테이블의 PK 값을 참조


범위(도메인) 무결성(Domain integrity)

  • 정의된 형식(범위)에서 관계형 DB의 모든 컬럼이 선언되도록 규정


shell_plus 실습

가. 생성, 내용추가, 저장 별도

$ python manage.py shell_plus

In [1]: comment = Comment()

In [2]: comment.content = 'first comment'

In [3]: comment.save()

IntegrityError: NOT NULL constraint failed: articles_comment.article_id
# 어떤 게시물을 참조하는지에 대한 정보가 없어서 에러 발생

# 참조 게시물 생성
In [4]: article = Article.objects.create(title='title', content='content')

In [5]: article.pk
Out[5]: 1

# 위에서 정의한 comment
In [6]: comment.content
Out[6]: 'first comment'

# comment에 참조하는 article 객체는 위에서 생성한 1번 article 인스턴스
In [7]: comment.article = article

In [8]: comment.save()

# comment가 잘 저장, 참조 객체도 확인 가능
In [9]: comment.pk
Out[9]: 1

# 참조 객체의 정보(속성값)도 확인 가능
In [10]: comment.article
Out[10]: <Article: title>

In [11]: comment.article.title
Out[11]: 'title'
  • 필드 참조 : comment.article_id = article.pk
  • 객체 참조(권장) : comment.article = article


나. 생성-내용, 저장

In [12]: comment = Comment(content='second comment', article=article)

In [13]: comment.save()

In [14]: comment.pk
Out[14]: 2

In [15]: comment.article.pk
Out[15]: 1

In [16]: comment.article_id
Out[16]: 1
  • comment.article.pkcomment.article_id를 혼동하지 않도록 주의
    • comment.article.pk : 현재 comment가 참조하는 article 인스턴스에서 가져오는 pk
    • comment.article_id : comment 생성시 article_id 필드에 저장되는 article 인스턴스의 pk
    • 같은 개념이지만 comment.article_pk 처럼 작성하지 않도록 주의


역참조 : comment_set

comment가 article을 참조하는데, article이 comment를 찾을 방법이 없나요?

외래키가 참조하는 모델의 인스턴스 + comment_set을 이용해 참조할 수 있다.

article.comment_set
  • article.comment_set manager 생성
  • 게시글에 몇 개의 댓글이 작성되었는지 ORM이 보장 불가능
    • article에는 comment가 없을 수도, 많을 수도 있음
    • 실제로 Article 클래스에는 Comment와의 어떠한 관계도 작성되어 있지 않음


In [1]: article = Article.objects.get(pk=1)

In [2]: article
Out[2]: <Article: title>

In [3]: article.comment_set.all()
Out[3]: <QuerySet [<Comment: first comment>, <Comment: second comment>]>

# 1번 article 인스턴스를 참조하는 댓글 queryset을 comments에 저장
In [4]: comments = article.comment_set.all()

# queryset의 각 comment의 content를 출력
In [5]: for comment in comments:
   ...:     print(comment.content)
   ...: 
first comment
second comment


게시글에서 댓글의 개수가 왜 중요하죠?

  • 참조의 경우, comment의 존재를 위해 article이 무조건 필요
  • comment.article 방식으로 조회가 가능
  • 반대의 경우, 이것이 불가능하므로 역참조 manager 생성


comment_set manager가 너무 길어요. 재정의할 수 있나요?

related_name 속성을 이용해 재정의할 수 있다.

# models.py / class Comment
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')


주의사항

  • migration을 다시 해야 함
  • 과거에 있던 comment_set의 model manager를 사용 불가
  • django의 1:N 관계에서는 related_name을 사용하지 않는 것을 권장
    • M:N에서는 related_name을 사용해야 하는 경우가 있음
    • 1:N에서는 _set 방식을 사용해서 구분하기 용이


Comment CREATE

코드

CommentForm 작성

# articles/forms.py
from .models import Article, Comment

class CommentForm(forms.ModelForm):
    
    class Meta:
        model = Comment
        fields = '__all__'
  • CommentForm : Comment model을 참조하는 ModelForm 정의


detail view 함수 수정

# articles/views.py
from .forms import ArticleForm, CommentForm

@require_safe
def 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/detail.html', context)
  • CommentForm을 import
  • comment_form의 빈 폼을 만들어 context로 template에 전달


detail template 수정

<!-- articles/detail.html -->
{% block content %}
  ...
  <form action="" method="POST">
    {% csrf_token %}
    {{ comment_form }}
    <input type="submit">
  </form>
{% endblock content %}
  • 댓글 폼을 표시하는 폼 태그 추가


문제상황 : ForeignKeyField를 직접 작성해야 되게 됐는데요?

image


CommentForm의 field에서 외래 키 출력을 제외한다.

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


댓글 생성 로직

urls

# articles/urls.py
app_name = 'articles'
urlpatterns = [
    ...
    path('<int:pk>/comments/', views.comments_create, name='comments_create'),
]


template

<!-- articles/detail.html -->
<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
  {% csrf_token %}
  {{ comment_form }}
  <input type="submit">
</form>
  • action의 URL을 채워준다.
  • 댓글폼이 채워지면 comments_create view 함수를 POST method로 호출


view

  • pk로부터 게시물을 받아와 저장
  • 폼 작성 데이터 기반으로 comment_form 인스턴스 생성
  • 유효하다면 저장
# articles/views.py
def comments_create(request, pk):
    article = Article.objects.get(pk=pk)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment_form.save()
    return redirect('articles:detail', article.pk)

이렇게 하면 되지 않을까?


IntegrityError가 발생하는데요?

앞서 ForeignKey field를 form에서 숨겨서 ForeignKey에 대한 정보가 없기 때문!


save() method : commit=False

  • DB에 저장되지 않은 인스턴스를 반환
  • 저장 전 객체에 대한 사용자 지정 처리를 수행할 때 필요
  • commit의 default는 True
# articles/views.py
def comments_create(request, pk):
    article = Article.objects.get(pk=pk)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        # commit False는 DB에 저장 없이 인스턴스만 반환해 comment에 저장
        comment = comment_form.save(commit=False)
        # 앞서 조회한 article 객체를 comment의 객체로 지정
        comment.article = article
        comment.save()
    return redirect('articles:detail', article.pk)


Comment READ

view

특정 article에 있는 모든 댓글을 가져온 후 context에 추가

# articles/detail.py

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


template

<!-- articles/detail.html -->
{% endblock content %}
  ...
  <a href="{% url 'articles:index' %}">back</a>
  <hr>
  <h4>댓글 목록</h4>
  <ul>
    {% for comment in comments %}
      <li>{{ comment.content }}</li>
    {% endfor %}
  </ul>
  ...
{% endblock content %}
  • for문을 이용해 comments queryset의 comment마다 content를 추가


Comment DELETE

url

# articles/urls.py

app_name = 'articles'
urlpatterns = [
    ...
    path('<int:article_pk>/comments/<int:comment_pk>/delete/', views.comments_delete, name='comments_delete'),
]
  • view에서 인스턴스 메서드를 통해 article_pk를 찾을 수 있다.
  • 하지만 variable routing을 이용해 article의 pk와 comment의 pk를 받는다.
  • urls.py의 일관성REST API의 규칙을 위해 위 방식 선택


view

# articles/views.py

def comments_delete(request, article_pk, comment_pk):
    comment = Comment.objects.get(pk=comment_pk)
    comment.delete()
    return redirect('articles:detail', article_pk)


template

<!-- articles/detail.html -->
<li>
  {{ comment.content }}
  <form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
    {% csrf_token %}
    <input type="submit" value="삭제">
  </form>
</li>
  • li 태그, 즉 댓글마다 삭제 버튼 추가
  • action URL에 article.pk, comment.pk variable 함께 전달


view 코드 최종

@require_POST
def comments_create(request, pk):
    if request.user.is_authenticated:
        article = get_object_or_404(Article, pk=pk)
        comment_form = CommentForm(request.POST)
        if comment_form.is_valid():
            comment = comment_form.save(commit=False)
            comment.article = article
            comment.save()
        return redirect('articles:detail', article.pk)
    return redirect('accounts:login')


@require_POST
def comments_delete(request, article_pk, comment_pk):
    if request.user.is_authenticated:
        comment = get_object_or_404(Comment, pk=comment_pk)
        comment.delete()
    return redirect('articles:detail', article_pk)
  • is_authenticated 및 POST method 등에 대한 shortcut 추가


Customizing authentication in Django

User 모델 대체

Substituting a custom User model

  • 특정 프로젝트에서는 Django의 내장 User 모델이 제공하는 인증 요구사항이 적절하지 않을 수 있다.
  • ex) username 대신 email을 식별 토큰으로 사용하는 것이 더 적합한 사이트

  • Django는 User를 참조하는데 사용하는 AUTH_USER_MODEL 값을 제공하여, default user model을 재정의(override)할 수 있도록 함

  • Django는 새 프로젝트를 시작하는 경우 기본 사용자 모델이 충분하더라도, 커스텀 유저 모델을 설정하는 것을 강력하게 권장(highly recommended)
  • 단, 프로젝트의 모든 migrations 혹은 첫 migrate를 실행하기 전에 이 작업을 마쳐야 함

“쓰든 안 쓰든 사람 일 모르니까 프로젝트 첫 마이그레이션 전에 커스텀 User 모델로 꼭 대체를 하고 시작해라!! 마이그레이션 하면 나중에 못 바꾼다!!”


AUTH_USER_MODEL

  • User를 나타내는 데에 사용되는 모델
  • 프로젝트가 진행되는동안 변경할 수 없음
  • 프로젝트 시작 시 설정하기 위한 것
  • 참조하는 모델은 첫 번째 마이그레이션에서 사용할 수 있어야 함
  • 기본 값: auth.User (auth 앱의 User 모델)


프로젝트 중간(mid-project)에 AUTH_USER_MODEL 변경

  • 정확히는 절대 불가능은 아니다.
  • 모델 관계에 영향을 미치기 때문에 불가능에 가까운 훨씬 더 어려운 작업을 요한다.


Custom User 모델 정의

가. 새 User 모델 정의

완전한 User 모델을 구현하는 기본 클래스인 AbstractUser를 상속받아 새로운 User 모델 작성

# accounts/models.py
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    pass


나. settings-AUTH_USER_MODEL 추가

  • Django가 사용하는 User모델
    • 기존 : auth 앱의 User 모델
    • 대체 : accounts 앱의 User 모델
  • 기본적으로 내장된 경로이므로 settings.py에 새로 넣어주어 덮어씌우면 된다.
# settings.py

AUTH_USER_MODEL = 'accounts.User'


admin site에 Custom User 모델 등록

# accounts/admin.py

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

admin.site.register(User, UserAdmin)

새 User 모델을 정의하며 admin 페이지에서 사라진 사용자(들) 탭을 다시 추가하는 코드


DB 초기화 및 마이그레이션

프로젝트 중간에 진행했기 때문에 제거 후 마이그레이션

  • db.sqlite3 파일 삭제
  • migrations 파일 모두 삭제(파일명에 숫자가 붙은 파일만 삭제, 폴더 지우면 안 됨)
$ python manage.py makemigrations

$ python manage.py migrate


Custom user & Built-in auth forms

바뀐 User 모델에 의해 발생한 문제들을 해결해보자


가. 회원가입 오류

문제점 : 회원가입이 안 됩니다

  • 회원가입에서 UserCreationForm(ModelForm)을 사용하기 때문
  • ModelForm의 class Meta에 model이 기존 내장 User 모델을 사용하기 때문
  1. 커스텀 ModelForm을 만들고 기존 Built-in Form을 상속
  2. model을 재정의
  3. 커스텀 ModelForm으로 기존 ModelForm 대체


forms.py

# accounts/forms.py
from django.contrib.auth.forms import UserChangeForm, UserCreationForm

class CustomUserCreationForm(UserCreationForm):
    
    class Meta:
        # 현재 django에서 활성화된 User 모델 가져옴
        model = get_user_model()
        # 상속하는 부모 클래스의 Meta class 내 fields, 그리고 내가 원하는 fields 추가
        fields = UserCreationForm.Meta.fields + ('email',)


views.py

# accounts/views.py
from .forms import CustomUserCreationForm

@require_http_methods(['GET', 'POST'])
def signup(request):
    if request.user.is_authenticated:
        return redirect('articles:index')

    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('articles:index')
    else:
        form = CustomUserCreationForm()
    context = {
        'form': form,
    }
    return render(request, 'accounts/signup.html', context)


나. 회원정보 수정 오류

문제 : 회원정보 수정이 안 됩니다

  • 위와 같은 이유
  • UserChangeForm를 상속하는 CustomUserChangeForm을 정의 후 overriding


get_user_model()

  • 현재 프로젝트에서 활성화된 사용자 모델(active user model)을 반환
  • User 모델을 커스터마이징한 상황에서는 Custom User 모델을 반환

  • 이 때문에 Django는 User 클래스를 직접 참조하는 대신 django.contrib.auth.get_user_model()을 사용하여 참조해야 한다고 강조
  • User를 직접 참조하면, User를 바꾸는 경우 기존 User 모델이 비활성화되며 문제 발생
  • 항상 활성화된 User 모델을 참조하는 get_user_model()을 사용하자!


1:N 관계 설정

User-Article(1:N)

Article model class에 user에 대한 정보를 설정해보자

# articles/models.py
from django.conf import settings

class Article(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 🔅
    title = models.CharField(max_length=10)
    ...
  • model에서의 user model 참조는 함수로 하지 않는다.
    • django.conf.settings import
    • settings.AUTH_USER_MODEL 인자 사용


User 모델 참조 인자 및 함수 ⭐

settings.AUTH_USER_MODEL

  • User 모델에 대한 외래키 또는 다대다 관계를 정의할 때 사용
  • return이 str
  • models.py에서 User 모델을 참조할 때 사용


get_user_model()

  • 현재 활성화(active)된 User 모델을 반환
  • return이 object
  • models.py가 아닌 다른 모든 곳에서 User 모델을 참조할 때 사용


Django에서 App이 실행되는 순서

  1. INSTALLED_APP에서 순차적으로 APP IMPORT
  2. 각 App의 models를 import
  • User 정의한 app의 models를 지나기 이전의 app들의 models에서 user를 사용하는 경우
  • 객체를 return하면 위험하고 str로 return해야 안전하기 때문에
  • settings.AUTH_USER_MODEL을 사용하여 str로 user model 참조


요점 ⭐

User 모델 참조시

  • models.py일 때 : settings.AUTH_USER_MODEL
  • models.py가 아닐 때 : get_user_model()


migration

models.py가 바뀌었으므로 migration해보자

  • 경고 오류 발생
  • null 값을 허용하지 않는 user_id 필드가 별도 값 없이 article에 추가되어서

  • 1 : 현재 화면에서 기본 값을 설정하겠다.
  • 2 : 일단 중단하고 default 정의하고 오겠다.
  • 1을 선택 후 그냥 admin user 번호인 1로 설정한다.


user 선택?

image

  • user, 즉 작성자를 선택하게끔 나오고 있다.
  • forms.py에서 이를 수정해보자.


forms.py

# articles/forms.py

class ArticleForm(forms.ModelForm):

    class Meta:
        model = Article
        # fields = '__all__'
        fields = ('title', 'content',)


Integrity 에러 생기는데요?

  • 게시글 작성 시 작성자 정보(article.user)가 누락되었기 때문
  • 위에서 외래키 선택을 Form에서 제거했을 때 상황과 같다.
  • create 함수에 로직을 추가


view 기존 코드

# articles/views.py

@login_required
@require_http_methods(['GET', 'POST'])
def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save()
            return redirect('articles:detail', article.pk)
    else:
        form = ArticleForm()
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)


view 수정 코드

# articles/views.py

@login_required
@require_http_methods(['GET', 'POST'])
def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False) 🔅
            article.user = request.user 🔅
            article.save() 🔅
            return redirect('articles:detail', article.pk)
    else:
        form = ArticleForm()
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)

작성자 정보 추가 및 저장


Delete

자신이 작성한 게시물만 삭제 가능하도록 설정

기존 코드

# articles/views.py

@require_POST
def delete(request, pk):
    if request.user.is_authticated:
        article = get_object_or_404(Article, pk=pk)
        article.delete()
    return redirect('articles:index')


수정 코드

# articles/views.py

@require_POST
def delete(request, pk):
    article = get_object_or_404(Article, pk=pk)
    if request.user.is_authticated:
        if request.user == article.user:
            article.delete()
            return redirect('articles:index')
    return redirect('articles:detail', article.pk)


Update

자신이 작성한 게시물만 수정 가능하도록 설정

기존 코드

# articles/views.py
@login_required
@require_http_methods(['GET', 'POST'])
def update(request, pk):
    article = get_object_or_404(Article, pk=pk)
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            article = form.save()
            return redirect('articles:detail', article.pk)
    else:
        form = ArticleForm(instance=article)
    context = {
        'article': article,
        'form': form,
    }
    return render(request, 'articles/update.html', context)


수정 코드

# articles/views.py
@login_required
@require_http_methods(['GET', 'POST'])
def update(request, pk):
    article = get_object_or_404(Article, pk=pk)
    if request.user == article.user: 🔅
        if request.method == 'POST':
            form = ArticleForm(request.POST, instance=article)
            if form.is_valid():
                article = form.save()
                return redirect('articles:detail', article.pk)
        else:
            form = ArticleForm(instance=article)
    else: 🔅
        return redirect('articles:index')
    context = {
        'article': article,
        'form': form,
    }
    return render(request, 'articles/update.html', context)


READ

게시글 작성 user가 누구인지 index.html에서 출력해보자

<!-- articles/index.html -->

{% for article in articles %}
  <p><b>작성자 : {{ article.user }}</b></p> 🔅
  <p>글 번호: {{ article.pk }}</p>  
  <p>글 제목: {{ article.title }}</p>
  <p>글 내용: {{ article.content }}</p>
  <a href="{% url 'articles:detail' article.pk %}">DETAIL</a>
  <hr>
{% endfor %}


해당 게시글의 작성자가 아니라면, 수정/삭제 버튼을 출력하지 않도록 해보자

기존 코드

<!-- articles/detail.html -->
<hr>
<p>제목 : {{ article.title }}</p>
<p>내용 : {{ article.content }}</p>
<p>작성 시각 : {{ article.created_at }}</p>
<p>수정 시각 : {{ article.updated_at }}</p>
<hr>
<a href="{% url 'articles:update' article.pk %}">수정</a>
<form action="{% url 'articles:delete' article.pk %}" method="POST">
  {% csrf_token %}
  <input type="submit" value="삭제">
</form>
<a href="{% url 'articles:index' %}">back</a>


수정 코드

<!-- articles/detail.html -->
<hr>
<p>제목 : {{ article.title }}</p>
<p>내용 : {{ article.content }}</p>
<p>작성 시각 : {{ article.created_at }}</p>
<p>수정 시각 : {{ article.updated_at }}</p>
<hr>
{% if user == article.user %} 🔅
  <a href="{% url 'articles:update' article.pk %}">수정</a>
  <form action="{% url 'articles:delete' article.pk %}" method="POST">
    {% csrf_token %}
    <input type="submit" value="삭제">
  </form>
{% endif %}
<a href="{% url 'articles:index' %}">back</a>


User-Comment(1:N)

models.py

# articles/models.py

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

    def __str__(self):
        return self.content
  • models를 바꿔주므로 migration을 실시


user 선택

image


forms.py

user field 제외

# articles/forms.py

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


user_id 없어서 추가

# articles/views.py

@require_POST
def comments_create(request, pk):
    if request.user.is_authenticated:
        article = get_object_or_404(Article, pk=pk)
        comment_form = CommentForm(request.POST)
        if comment_form.is_valid():
            comment = comment_form.save(commit=False)
            comment.article = article
            comment.user = request.user 🔅
            comment.save()
        return redirect('articles:detail', article.pk)
    return redirect('accounts:login')

FK를 2개 넣는 상황 : user_id, article_id


권한에 대한 작업

가. 비로그인 유저에게는 댓글 form 출력 숨기기

기존 코드

<!-- articles/detail.html -->
<form action="{% url 'articles:comments_create' article.pk %}" method="POST">
  {% csrf_token %}
  {{ comment_form }}
  <input type="submit">
</form>


수정 코드

<!-- articles/detail.html -->
{% if request.user.is_authenticated %}
  <form action="{% url 'articles:comments_create' article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form }}
    <input type="submit">
  </form>
{% else %}
  <a href="{% url 'accounts:login' %}">[댓글을 작성하려면 로그인하세요.]</a>
{% endif %}


나. 댓글 작성자 출력

<!-- articles/detail.html -->
<h4>댓글 목록</h4>
<ul>
  {% for comment in comments %}
    <li>
      {{ comment.user }} - {{ comment.content }} 🔅
      <form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
        {% csrf_token %}
        <input type="submit" value="삭제">
      </form>
    </li>
  {% endfor %}
</ul>


다. 자신이 작성한 댓글만 삭제 버튼 보이기

<!-- articles/detail.html -->
<h4>댓글 목록</h4>
<ul>
  {% for comment in comments %}
    <li>
      {{ comment.user }} - {{ comment.content }}
      {% if user == comment.user %} 🔅
        <form action="{% url 'articles:comments_delete' article.pk comment.pk %}" method="POST">
          {% csrf_token %}
          <input type="submit" value="삭제">
        </form>
      {% endif %}
    </li>
  {% endfor %}
</ul>


라. 자신이 작성한 댓글만 삭제

# articles/views.py

@require_POST
def comments_delete(request, article_pk, comment_pk):
    if request.user.is_authenticated:
        comment = get_object_or_404(Comment, pk=comment_pk)
        if request.user == comment.user: 🔅
            comment.delete()
    return redirect('articles:detail', article_pk)

댓글남기기