해리의 데브로그

Django 22 - 이미지 다루기 1(폼셋, 트렌젝션)

|

이미지 다루기 1 (인스타그램)

Django17 미디어 파일 관리 및 업로드 구현 포스팅에서 이미지(미디어)파일을 어떻게 업로드하여 관리하고, 재가공하는지에 대해 정리해보았다. 추가로, 하나의 게시글(Post)에 여러 사진을 올리고, 또한 그사진들을 횡으로 보여지도록, 마치 인스타그램 처럼 기능하게끔 내용을 확장키켜보도록 하자.

하나의 Post에 여러개의 사진을 올리기

Models.py

하나의 Post에 여러 사진을 올리기 위해서는 Post와 이미지를 1: N 관계를 형성해야함. 데이터베이스 관계를 재 설정해야하므로 models.py를 다시 수정하도록 하자.

  • Image 라는 새로운 클래스 생성 후 , Post 클래스 내 image 필드의 내용을 그대로 갖고와 Image 클래스 내 file 필드값으로 옮김.
  • Post 클래스와 image 클래스의 관계를 1:N로 설정
    • post = models.ForeignKey(Post, on_delete=models.CASCADE)
  • 이미지 저장 경로 변경({instance.content} => {instance.post.content})
    • 본래 image가 Post 클래스의 하위 필드인 경우, instance.content로 접근 가능했으나, 이미지는 Image 클래스로 이동되었으므로, instance.post.content로 한번 더 접근해야함.
def path_image_path(instance, filename):
    #{instance.content} => {instance.post.content}
    return f'posts/{instance.post.content}/{filename}'

class Image(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    file = processedImageField(
        upload_to = path_image_path,
        processors = [ResizeToFill(600,600)],
        format = 'JPEG',
        option = {'quality':90},
    )

Forms.py (1)

  • PostFormimage 필드 삭제. 이에 따라 게시글 생성 페이지에서는, PostForm 양식에는 이미지 업로드 기능이 사라졌으므로, Image 클래스를 갖고와 ImageForm 을 만들어서, html에 넘겨줘야함.
  • Image 클래스 import
#Image 클래스 추가
from .models import Post, Comment, Image

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        # image 필드 삭제
        fields = ['content',]

class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['file',]

Forms.py (2)

위의 코드대로 ImageForm 을 html 문서에 넘길 경우에는 하나의 이미지만 올릴 수 있음. 여러개의 이미지를 올리기위해서는 formset을 사용해야함.

  • inlineformset_factory : 이미지 폼을 갖고와 엮어서 폼셋으로 만들어주는 역할을 함.
    • 1번째 인자: 만들 데이터의 부모 모델(1:N에서 1을 의미). 이미지를 들고 있는 모델(Post)
    • 2번째 인자: 우리가 만들 모델(Image)
    • 3번째 인자(키워드 인자, form= 폼 명): 기본으로 들고오는 폼이미지 폼
    • 4번째 인자(키워드 인자, extra= 갯수): 이미지의 갯수
  • 공식문서 참조
class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['file',]

#추가
ImageFormSet = forms.inlineformset_factory(Post, Image, form=ImageForm, extra=3)

Views.py (1)

  • 게시글을 생성할 때, 이미지를 업로드해야하므로, create 함수 코드 수정
  • ImageFormSet import
  • GET 방식인 경우, ImageFormSet 을 갖고와 변수(image_formset)에 저장한 후 , 템플릿 변수로 넘김
from .forms import PostForm, CommentForm, ImageFormSet

@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST, request.FILES)
        if post_form.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            post.save()
        return redirect('posts:list')
    else:
        post_form = PostForm()
        image_formset = ImageFormSet()
    return render(request, 'posts/create.html', {
                                    'post_form': post_form, 
                                    'image_formset':image_formset,
                                    })

Views.py (2)

  • POST 방식인 경우에도 마찬가지로 ImageFormSet 에 대한 코드를 삽입해야함.
  • post_form 의 두번째 인자에 들어간 request.FILES은 삭제한 후, ImageFormSet 을 불러와 인자에 각각request.POSTrequest.FILES 을 입력.
  • image_formset 또한 유효성 검증을 통과해야하므로 조건문에 추가.
  • PostImage 는 1:N 관계로, 게시글이 존재해야 이미지를 업로드 할 수 있으므로 순서가 매우 중요하다. 따라서 post.save() 로 게시글을 먼저 생성한 후 id 값이 부여 되면 post가 외래키로 사용가능하게되고 Image 모델에 따라 이미지를 저장 시킬 수 있게 된다.
  • 또한, image_formset 은 모델(클래스) => 모델폼 으로 래핑이 되어있는데, 이는 일종의 외부에 껍데기가 씌인 개념이다. 따라서 image_formset.instance 로 접근하여 껍데기를 벗긴 후, 부모 모델의 인스턴스를 저장시켜줘야 한다. (만약 래핑이 되어 있지 않다면 image_formset.post = post로 접근이 가능할 것이다.)
@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES)
        if post_form.is_valid() and image_formset.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            post.save()
            image_formset.instance = post
            image_formset.save()
        return redirect('posts:list')

Views.py (3) - transaction

위의 코드대로 실행을 시킬 경우, 작동이 될 수도 있으나, 이는 완벽한 코드가 아니며 많은 에러가 발생 할 수 있다.

.save() 는 데이터를 실제 DB에 저장시킬 때 사용하는 코드로, 이 코드를 통해 Django는 DB에 데이터를 저장해달라고 요청을 보내게 된다. 데이터는 db.SQLITE3 라는 파일에 저장되는데, 이 때 DB인 db.SQLITE3는 최상단 디렉토리에 위치하고 있다 (Django 밖에 위치)

Django ORM이 DB에 요청을 보내 놓고, DB가 역으로 응답을 다시 보내지 않는 이상, Django 는 응답을 기다리지 않고 바로 밑의 코드를 실행시키게 된다. (이를 비동기로 동작한다고 한다)

post.save() 코드의 경우, DB가 따로 응답을 보내지 않기 때문에 DB에 실제로 저장이 완료되었는지를 알 수 없다. 예를 들어, 병목 현상으로 인해 DB에 저장되는 시간이 많이 소요될 수 도 있을 것이다. 따라서, post 가 DB에 저장되지 않았는데, image_formset.instance = post 의 코드가 실행되는 상황이 발생 할 수 있다.

DB의 입장에서는 순서가 보장되지 않기 때문에(ex, 이미지 저장 후, post 저장) Django에서는 위와 같은 상황이 발생하는 것을 막아줘야할 필요가 있음. 이러한 역할을 하는 Django 의 transaction이며 아래와 같이 코드를 작성할 수 있다.

  • with transaction.atomic()
  • from django.db import transaction 사용을 위해 관련 코드 import

transaction은 간단히 말해 작업단위(쪼개질 수 없는 업무처리 단위) 라 할 수 있는데, 여러개의 프로세스가 묶여져 마치 하나처럼 동작하는 방식이라 할 수 있음. 데이터 베이스 충돌을 해결하기 위해서, 둘또는 그이상의 데이터베이스 업데이트를 하나의 작업으로 처리하는 기법을 의미한다.

그렇기 때문에 성공 아니면 실패 두가지 결과밖에 존재하지 않음. - Transaction의 원자성(Atomicity)

관련 하기 reference를 추가로 참조해보자!

from django.db import transaction

@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES)
        if post_form.is_valid() and image_formset.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            
            # from django.db import transaction
            with transaction.atomic():
                # 첫번째: 실제 DB에 저장
                post.save()
                # 두번째
                image_formset.instance = post
                image_formset.save() #실제 DB에 저장
                return redirect('posts:list')

views.py 최종 version

  • 최종적으로 정리된 코드는 다음과 같음.
from django.db import transaction

@login_required
def create(request):
    if request.method == 'POST':
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES)
        if post_form.is_valid() and image_formset.is_valid():
            post = post_form.save(commit=False)
            post.user = request.user
            
            with transaction.atomic():
                post.save()
                image_formset.instance = post
                image_formset.save() 
                return redirect('posts:list')
    else:
        post_form = PostForm()
        image_formset = ImageFormSet()
        
    return render(request, 'posts/create.html', {
                                    'post_form': post_form, 
                                    'image_formset':image_formset,
                                    })            

create.html

  • 템플릿 변수로 넘긴 모델 폼셋(image_formset)을 진자 템플릿 문법을 통하여 입력
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>New Post</h1>

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {% bootstrap_form post_form %}
    <!--추가된 부분 -->
    {% image_formset.as_p %}
    <!--추가된 부분 -->
    <input type="submit" value="Submit"/>
</form>
{% endblock %}

_post.html

  • 업로드한 이미지 표시. 1:N 관계 중, 1(post)에서 N(image) 으로 접근할 때는 항상 _set.all을 사용하였음.
    • 예) post.image_set
  • post.image_set 에는 여러장의 이미지가 저장되어있으므로 반복문을 돌려서 표시.
  • img 태그의 src 속성에는 반드시 url까지 입력하도록 하자.
<div class="card" style="width: 18rem;">
  <div class="card-header">
    <span> <a href="{% url 'accounts:people' post.user.username %}">{{ post.user.username }} </a></span>
  </div>
  {% for image in post.image_set.all %}
  	<img src="{{ image.file.url }}" class="card-img-top" alt="{{ image.file.image }}">
  {% endfor %}

Django 21 - M:N 관계설정 (좋아요 기능) / 개인 페이지 만들기

|

M:N 관계 설정 (좋아요 기능)

1:N 관계는 하나의 모델(N)에 대하여 하나의 모델만이 연결이 될 수 있음. 그러나 일반적으로 “좋아요” 기능의 경우, 한명의 유저(user)가 여러 게시글(post)에 “좋아요”를 누를 수 있으며, 반대로 하나의 게시글(post)에도 여러명의 유저(user)가 좋아요를 누를 수 있어야함. 따라서 1:N 관계가 아니라 다대다(M:N) 관계 설정이 필요함.

POST1 [P1, U1] USER1
POST2 [P2, U2] USER2
	  [P2, U3] USER3
POST3 [P3, U3] USER3
POST4 [P3, U4] USER4

모델 클래스 내 필드 추가

  • M:N 관계를 설정하기 위해 ManyToManyField 사용
  • 관계되는 모델을 첫번째 인자에 입력. Django 내 내장된 유저모델을 사용하기 위해 settings.AUTH_USER_MODEL 을 입력.
  • 관계되는 변수명은 related_name 속성값으로 표시. 이는 첫번째 인자에서 접근할때 사용하는 변수명이며, 아래와 같이 쓸 수 있음.
    • Post => post.like_users
    • User => user.like_posts
  • 이후, 마이그레이션 재설정
class Post(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    content = models.TextField()
    image = ProcessedImageField(
                    upload_to= post_image_path, 
                    processors=[ResizeToFill(600,600)], 
                    format='JPEG', 
                    options= {'quality': 90 },
        )
    like_users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='like_posts')

views.py & urls.py 코드 작성(좋아요 함수 구현)

Post 모델 클래스에 새로 생성한 like_users 필드는 리스트의 형태를 띄고 있음. 이는 M:N 관계에 따라, like_users 에 하나의 값만 저장되는게 아니라 여러 값이 저장 될 수 있기 때문임. 이를 활용하여, 동일한 url과 동일한 함수를 사용하여, 조건문 분기를 통해 요청을 보내는 유저가 like_users 리스트에 포함되지 않았을 경우, 리스트에 유저를 추가 한 후, 좋아요 기능이 활성화되고, 반대의 경우는 유저를 리스트에서 제거하여 좋아요 기능을 비활성화 시키면 됨.

  • post.like_users.all() : 인스턴스 객체인 postlike_users 안의 모든 유저정보를 갖고 옴.
  • 조건문을 이용하여 좋아요 기능 활성화/비활성화를 나눔.
#views.py
def like(request, post_id):
    post = get_object_or_404(Post, id=user_id)
    
    if request.user in post.like_users.all():
        #좋아요 취소
        post.like_users.remove(request.user)
    else:
        post.like_users.add(request.user)
    
    return redirect('posts:list')

#urls.py
urlpatterns = [
    path('<int:post_id>/like/', views.like, name='like'),
]

_post.html 수정

font Awesome 은 벡터 기반의 웹폰트 아이콘 제공하는 사이트로, CDN을 활용하여 아이콘을 웹페이지에 삽입 시킬 수 있음. 원하는 아이콘을 찾아서 html 문서 안에 코드를 삽입하면 됨. 좋아요 기능을 나타내기위해 꽉찬 하트와 빈 하트를 각각 사용.

  • 조건문을 통해 현재 userposts.like_users 리스트 안에 있을 경우(좋아요가 이미 된 경우), 좋아요 이모티콘(꽉찬 하트) 표시. 그렇지 않은 경우(좋아요가 되지 않은 경우), 빈 하트 이모티콘 표시
  • 추가적으로 post.like_users.count 를 통해 좋아요의 갯수를 카운팅 할 수 있음.
  • fontAwesome을 사용하기 위한 CDN 코드를 base.html에 삽입하자.
<!--좋아요 기능-->
<a href="{% url 'posts:like' post.id %}">
  {% if user in post.like_users.all %}
    <a href="{% url 'posts:like' post.id %}"><i class="fas fa-heart"></i></a>
  {% else %}
    <a href="{% url 'posts:like' post.id %}"><i class="far fa-heart"></i></a>
  {% endif %}
</a>
<p class="card-text">
    <!--좋아요 갯수 카운트-->
    {{ post.like_users.count }} 명이 좋아합니다.
</p>


개인 페이지 만들기

인스타그램의 개인 계정페이지와 같은 개인 페이지를 만들어보도록 하자. 계정에 대한 페이지이므로, posts가 아닌 accounts 어플리케이션 내 views.py에 함수를 정의하는 것이 적합함.

views.py 작성

  • get_object_or_404 사용을 위해 import
  • get_user_model : settings 안의 auth의 user 모델을 불러옴. 사용을 하기 위해 import
    • settings.AUTH_USER_MODEL은 모델 클래스를 반환하는 것이 아니라 모델 명을 string 형태로 반환하므로, get_user_model 대신 사용할 경우, ValueError 가 발생한다.
  • 앞의 usernameuser모델의 username을 말하며, 뒤의 username은 주소로 들어오는 username을 말함.
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import get_user_model

def people(request, username):
    #get_user_model() => User 클래스를 호출함
    people = get_object_or_404(get_user_model(), username = username)
    return render(request, 'accounts/people.html', {'people':people})

urls.py 작성

  • urls.py 내 url 경로를 설정할 때, username을 variable routing으로 사용하므로 str 타입으로 작성
urlpatterns = [
    path('people/<str:username>/', views.people, name='people'),
] 

people.html 생성

{% extends 'base.html' %}
{% block container %}

<h1>{{ people.username }}</h1>

{% endblock %}

_post.html 수정

_post.html 에 각 게시글마다 게시글에 대한 유저정보가 뜨도록 코드를 작성해놓았음 (Django18 - 게시글에 유저정보 표시하기 참조). 유저정보는 post.user.username 을 통해 표시되어있으며, 이를 활용하여 개인 페이지로 들어가는 링크와 연결 시킬 수 있음

  • Boostrap card를 이용하여 양식을 다듬었음.
<div class="card" style="width: 18rem;">
  <div class="card-header">
    <span> <a href="{% url 'accounts:people' post.user.username %}">{{ post.user.username }} </a></span>
  </div>

Django 20 - 댓글 CRUD (2)

|

댓글 CRUD (2)

댓글 삭제하기 (Delete)

urls.py의 댓글 삭제 url에 post_idcomment_id 2개를 받아 넘기므로 이에 따라 2개의 variable routing이 필요하다. 따라서, url 경로에 명기된 variable routing 순서에 맞춰서 views.py 함수의 인자로 넘겨 줘야한다.

  • require_http_methodimport 하여 GET 또는 POST 방식의 경우 코드가 실행되게 데코레이터 설정. (기본적으로는 POST 방식이 적합하나, 코드 작성의 편의성을 위해 GET 방식으로 코드구현)
  • Comment 모델의 멤버 변수인 commnet.user 이 현재 요청을 보내는 user 와 일치하지 않는 경우, 댓글이 삭제되지 않고 다시 리스트 페이지가 뜨도록 redirect 설정
  • 반대의 경우(user 정보가 일치할 경우), DB에서 해당 댓글을 삭제시킴
from django.views.decorators.http import require_POST, require_http_methods

@require_http_methods(['GET', 'POST'])
def comment_delete(requests, post_id, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)
    if comment_user != request.user:
        return redirect('posts:list')
   	comment.delete()
    return redirect('posts:list')

#urls.py
urlpatterns = [
    path('<int:post_id>/comments/<int:comment_id>/delete/', views.comment_delete, name="comment_delete")
]
  • 동일한 방식으로 comment.useruser와 일치한 경우에만 삭제 버튼이 보이도록 _post.html 코드 수정
    • 댓글을 보여주는 반복문 안에서 삭제 링크를 작성
<div class="card-body">
    {% for comment in post.comment_set.all %}
    <div class="card-text">
        <strong> {{ comment.user.username }} </strong> {{ comment.content }}
        {% if comment.user == user %}
        <a href="{% url 'posts:comment_delete' post.id comment.id %}">댓글 삭제</a>
        {% endif %}
    </div>
    {% empty %}
    <div class="card-text">댓글이 없습니다.</div>
    {% endfor %}
</div>

댓글 수정하기(Update)

views.py 내 댓글을 수정하는 코드는 게시글을 수정하는 코드를 작성했던 방식과 매우 유사함.

  • get_object_or_404 & Comment 모델을 이용하여 원하는 댓글에 대한 정보를 갖고 옴.
  • HTTP Method 가 GET 인 경우
    • commentForm 의 instance에 현재 인스턴스 객체의 값을 넣어준 후, comment_form 변수에 저장시키고, comment_update.html에 템플릿 변수로 넘김. 브라우저를 통해 html 문서에 접근하면 기존에 저장되있는 정보를 확인 할 수 있음.
  • HTTP Method가 POST인 경우
    • comment_html 에서 내용을 수정한 후 제출할 경우, form 태그 내 method 속성값에 따라, POST 방식으로 데이터를 요청보내게 되고, request.method == 'POST' 이하 코드가 실행된다.
    • CommentForm의 첫번째 인자 request.POST 에는 수정된 데이터의 정보가 들어가 있으며, 2번째 인자인 instance=comment 에는 어떤 댓글인지에 대한 정보가 담겨있음.
    • 유효성 검증을 통과한 경우, DB에 수정된 내용을 저장시키고, 리스트 페이지로 redirect 시킴.
#views.py
def comment_update(request, post_id, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)

    if request.user != comment.user:
        return redirect('posts:list')

    if request.method == 'POST':
        comment_form = CommentForm(request.POST, instance=comment)
        if comment_form.is_valid():
            comment_form.save()
        return redirect('posts:list')
    
    else:
        comment_form = CommentForm(instance=comment)
        return render(request, 'posts/comment_update.htm', {'comment_form':comment_form})
 
#urls.py
urlpatterns = [
    path('<int:post_id>/comment/<int:comment_id>/update/', views.comment_update, name="comment_update"),
]
  • 댓글 수정 코드와 동일한 방식으로 comment.useruser와 일치한 경우에만 수정 버튼이 보이도록 _post.html 코드 수정
    • 댓글을 보여주는 반복문 안에서 삭제 링크를 작성
<div class="card-body">
    {% for comment in post.comment_set.all %}
    <div class="card-text">
        <strong> {{ comment.user.username }} </strong> {{ comment.content }}
        {% if comment.user == user %}
        <a href="{% url 'posts:comment_delete' post.id comment.id %}">댓글 삭제</a>
        <a href="{% url 'posts:comment_update' post.id comment.id %}">댓글 수정</a>     
        {% endif %}
    </div>

2019년 6월 3주차 TIL

|

2019-06-10

  • 댓글 기능 관련 복습(Create, Read)을 이어서 진행하였음.
  • 모델폼을 사용하면, 구현한 모델을 활용하여 html 파일에서 그대로 사용할 수 있는 이점이 있는데, input 태그에 클래스를 추가하는 등, html 문서에서는 손쉽게 할 수 있었던 작업이, 모델 폼을 쓸 경우에는 모델폼내에 오버라이딩을 통해 다시 속성값을 정의해줘야하는 부수적인 일이 필요하다.
  • 관련 내용을 정리하다보니, 내가 배운 방법이 아니더라도 다른 방법을 통해서 속성값을 부여할 수 있다는 것을 알게 되었다. 오버라이딩 하는 코드가 손에 익지 않아 몇번을 쓰고 지우고를 반복했는데, 새로운 방법이 되려 더 쉽고 직관적으로 이해가 되는 듯 하다. 무조건 배운대로 이 방법만 된다보다는, 좀 더 유연하게 생각해보려 노력하자
#방법 1
class CommentForm(forms.ModelForm):
    content = forms.CharField(label='', widget=forms.TextInput({attrs={'class':'form-control', 'placeholder': '댓글을 작성하세요.'}}))
    class Meta:
        model = Comment
        fields = ['content',]
 
#방법 2
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['content']
        widgets = {
            'content': forms.TextInput(attrs={'placeholder':'댓글을 입력하세요'})
        }

2019-06-11

  • Django 댓글 CRUD 중 Update와 Delete에 대해 복습을 진행하였음.
  • 전반적인 메커니즘은 기본 CRUD와 거의 유사했으며, Update의 경우, 이전에 자바스크립트를 이용하여 다른 페이지로 넘기지 않고 동적으로 구현하려다가 시간적 여유가 없어서 마무리 못지었던것이 기억난다. 이후에 자바스크립트 복습을 진행할 때 다시한번 코드를 짜봐야겠다.
  • 다시 코드를 작성하면서 알게된건데, post_idcomment_id 를 둘다 variable routing으로 안넘겨도, 댓글에 대한 Update와 Delete 기능이 제대로 작동하는 듯 하다. post_id가 필수라고 생각했는데, 굳이 post_id가 없더라도 comment 인스턴스 객체 자체가 고유 id값을 갖고있기 때문에, comment_id 값만 있어도 충분한 듯하다.

2019-06-12

  • 수업시간에 전공자반과 함께 조를 짜서 아이디어 해커톤을 진행하였음. 확실히 전공자들이 깊은 도메인 지식과 문제를 접근하는 방식에 대한 이해도가 높은 것을 알 수 있었다. 많은 자극이 되었음!
  • Google Search Console 에서 블로그에 모바일 사용 편의성에 대한 문제가 발생했다는 메일을 받게되어 블로그 템플릿 코드를 살짝 다시 다듬었다. 처음 이 템플릿을 받아와 코드 내용도 모른채 깃허브 블로그를 해보겠다고 며칠을 헤멧었는데, 몇개월이 지난 지금 다시 코드를 살펴보니 많은 부분에 대한 이해도가 높아짐을 알 수 있었다. 조만간 코드를 좀 더 분석해서 더 많은 기능을 넣어봐야겠다.

2019-06-13

  • 좋아요 기능을 통해 M:N 관계에 대한 복습을 진행함. 다대다 관계 설정시에는 ManyToManyField 를 사용
    • 첫번째 인자로는 연결하는 모델 클래스를 입력
    • 두번째 인자로는 연결되는 모델 클래스에서 접근할때 쓰는 naming을 설정함(related_name)
  • Django에서 유저모델을 불러올 때, get_user_modelsettings.AUTH_USER_MODEL 를 쓰는 경우가 있는데, 미세한 차이가 있음을 알 수 있었음.
    • get_user_model : settings 안의 auth의 user 모델을 불러옴. 사용을 하기 위해 import
    • settings.AUTH_USER_MODEL은 모델 클래스를 반환하는 것이 아니라 모델 명을 string 형태로 반환. 따라서, settings.AUTH_USER_MODEL 로 views.py 내 유저모델을 부를 경우, ValueError가 발생함. (외래키로 유저모델을 불러올땐 Django 공식문서에 따르면, 이 방법을 권장함)

2019-06-16

  • 이미지폼셋을 이용하여 하나의 모델폼으로 여러장의 사진을 동시에 올리는 코드에 대해 공부하였음.
  • inlineformset_factory 메서드를 이용하여 외래키를 통해 관련객체를 작업하는 경우를 단순화시킴. (이미지 모델폼을 갖고와 폼셋으로 만들어 여러 이미지를 동시에 올리게끔 해줌)
  • formset 에 대하여 아주 얕게, 기본적인 개념만 배우고 넘어갔는데, modelformsetinlineformset 은 구체적으로 어떻게 다른지, 또 factory method에 다양한 키워드 인자에 대해서도 시간내서 상세히 공부해봐야겠다.
  • transaction: 쪼개질수 없는 업무처리 단위. 데이터 베이스 충돌을 해결하기 우해서 여러개의 프로세스를 묶어서 하나의 작업으로 처리하는 기법을 의미함.
    • Transaction의 원자성을 이용한 코드로, 장고의 비동기 동작으로 인해 발생할 수 있는 에러상황을 방지하는 부분을 살펴보았음. - ` with transaction.atomic()`

Django 19 - 1:N 관계설정 / 댓글 CRUD (1)

|

1:N 관계 설정 & 댓글 CRUD (1)

1:N 관계 설정을 통해 각 게시글에 댓글을 쓰는 코드를 작성해보자. 유저와 댓글간의 1:N 및 게시글과 댓글 간의 1:N, 이중 1:N 관계를 띄도록 코드를 작성 할 수 있음.

기본설정하기

Comment 모델 클래스 생성

  • 어떤 유저가 쓴 댓글인지 & 어떠한 게시글에 달린 댓글인지에 대한 1:N 관계 설정
    • 클래스는 posts 어플리케이션 내에 작성
  • models.ForeignKey 의 첫번째 인자로는 외래키가 연결되는 테이블을 입력함.
    • settings.AUTH_USER_MODEL 에는 유저 테이블에 대한 정보가 담겨 있음.
  • 두번째 인자에는 ForeignKeyField 가 바라보는 값이 삭제될 때 어떻게 처리할지를 옵션으로 정함
    • CASCADE : 부모가 삭제 되면, 자기 자신도 삭제
    • PROTECT : 자식이 존재하면, 부모 삭제 불가능 ( ProtectedError 발생시킴)
    • SET_NULL : 부모가 삭제되면, 자식의 부모 정보를 NULL로 변경
from django.conf import settings

class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    content = models.TextField()
  • 스키마 재설정(makemigrations & migrate )

Comment 모델에 대한 모델폼 생성

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    model = Comment
    fields = ['content',]

댓글 생성 및 읽기(Create & Read)

댓글 생성하기

  • models.py & forms.py 내 새로 생성한 클래스인 CommentCommentForm을 각각 import
  • 앞 포스팅에서 views.py - create 함수를 통해 게시글을 생성할 때, 유저정보를 입력하였던 방법을 동일하게 호활용하여, Comment - CommentForm 으로 wrapping 되어있는 데이터를 한번 벗겨 낸 후, comment의 인스턴스 객체의 멤버변수인 user 과 post에 각각 데이터를 집어넣음.
    • comment의 멤버변수인 postPost 모델과 외래키로 연결되어있으며, 이때 자동적으로 연결된 post에 대한 id를 입력하는 column이 생성된다. Comment 모델 클래스를 보면 post 필드라고 명시 되어있지만 실제로 생성되는 필드는 post가 아니라 post_id 이다. (Default 값)
  • comment.post_id 에서 post_id 는 함수의 2번째 인자인 post_id 와는 무관함을 유의하자.
    • 함수의 2번째 인자로 들어오는 post_id 는 urls.py를 통해 들어오는 variable routing을 의미.
    • comment.post_idcomment 모델에서 외래키로 연결된 post의 id 값을 저장시키는 필드명을 나타냄.
#views.py
from .models import Comment
from .forms import CommentForm

def comment_create(request, post_id):
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = Comment_form.save(commit=False)
        comment.user = request.user
        comment.post_id = post_id
        comment.save()
    return redirect('posts:list')

#urls.py
urlpatterns = [
    path('<int:comment_id>/comments/create/', views.comment_create, name='comment_create'),
]
  • 위의 코드는 아래와 같은 방법으로도 나타낼 수 있음. 그러나 DB를 접속하여 어떠한 게시글인지에 대한 정보를 한번 불러오는 등에 불필요한 과정이 포함되어있으므로 비효율적임.
def comment_create(request, post_id):
    #DB에서부터 어떠한 게시글인지에대한 정보를 찾아서 post변수에 저장
    post = get_object_or_404(Post, id=post_id)
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = comment_form.save(commit=False)
        comment.user = request.user
        #comment.post를 통해 comment에 대한 1:N 관계 설정
        comment.post = post
        comment.save()
        
    return redirect('posts:list')

HTTP methods에 대한 데코레이터 설정

이전 포스팅에서 @login_required 데코레이터를 사용하여 로그인이 되어야만이 함수가 실행되게끔 설정을하였던 것처럼, HTTP methods에 대한 데코레이터 설정이 가능함. 해당 데코레이터 사용을 위해 관련코드 import`

  • from django.views.decorators.http import require_POST
  • import 이하에는 원하는 HTTP methods를 입력해주면 됨
    • require_POST: POST 방식으로 주소를 접근할때만 함수를 실행하게 설정
    • require_GET : GET 방식만 가능
    • require_safe :GET 방식과 HEAD방식만 가능
    • require_http_methods(['GET','POST']) : GET, POST방식 사용 가능
  • 위의 방법을 사용하면 if request.method == 'POST' 와 같은 조건문을 사용할 필요가 없음
  • 공식문서 참조
from django.views.decorators.http import require_POST

@require_POST
def comment_create(request, post_id):
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = comment_form.save(commit=False)
        comment.user = request.user
        comment.post_id = post_id
        comment.save()
        
    return redirect('posts:list')

댓글 표시하기(views.py)

  • 리스트 페이지에 댓글을 보여주기 위해 list 함수 수정
  • forms.py 내 생성한 CommentForm 을 사용하기 위해 불러온 후, 템플릿 변수로 넘겨줌
  • 추가로, 최근에 작성한 게시글이 상위에 나타내기 위해 order_by('-id') 입력
def list(request):
    posts = Post.objects.order_by('-id').all()
    comment_form = CommentForm()
    return render(request, 'posts/list.html', {'posts':posts, 'comment_form': comment_form})

댓글 표시 & 생성(_post.html)

  • 댓글 정보 및 생성기능을 실제로 브라우져에 나타내기 위한 코드를 _post.html 에 작성
  • urls.py 에 작성된 url 경로에 따라 함수가 실행될 수 있도록 form 태그 및 action 속성 경로 설정
    • {% if user.is_authenticated %} 을 사용하여 로그인이 된 경우만 댓글생성이 보이도록 함.
  • 댓글 목록도 동시에 출력 (부트스트랩 레이아웃 적용)
    • {% for comment in post.comment_set.all %} 진자템플릿을 이용하여 반복문을 실행함.
    • 실제 코드는 post.comment_set.all() 으로 작성되어야 하나, 진자템플릿 문법에는 () 이 생략됨.
    • 반복문이 비어있을 경우, {% empty %} 이하 코드 실행됨.

※ Post와 Comment는 models.py를 통해 1:N 관계가 설정되어있으며, 각각의 위치에서 관계된 다른반대쪽을 불러오는 방법은 아래와 같음

  • Post에서 Comment로 접근할 때(1 => N) : post.comment.set.all()
  • Comment에서 Post로 접근할 때(N => 1) : comment.post
<!-- _post.html -->

<div class="card-body">
    {% for comment in post.comment_set.all %}
      <div class="card-text">
        <strong> {{ comment.user.username }} </strong> {{ comment.content }}
      </div>
    {% empty %}
      <div class="card-text">댓글이 없습니다.</div>
    {% endfor %}
</div>

{% if user.is_authenticated %}
<form action="{% url 'posts:comment_create' post.id %}" method="POST">
   {% csrf_token %}
   {{ comment_form }}
   <input type="submit" value="Submit"/>
</form>
{% endif %}

모델 필드속성 오버라이딩 설정

Model은 실제 DB에 저장 시키기 위해 사용(스키마의 형태)하는 역할을 함. Comment 모델의 content 필드의 경우 TextField를 사용하였으나, 실제 댓글은 길이의 제한이 없는 TextField 보다는 길이제한을 둘 수 있는 CharField가 좀 더 적합하다. 따라서 content의 필드 속성을 TextField 에서 CharField 로 변경하는 작업을 forms.py에서 할 수 있음.(https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#overriding-the-default-fields)

  • label : 기본적인 input의 이름. 빈 값을 넣어서 라벨을 제거.
  • widget : 속성값을 줄 때 사용. widget의 속성을 forms.TextInput 이나 forms.Textarea 등 형태를 설정할 수 있음.
    • TextInput 의 속성을 괄호안에 사용. attrs={} 와 같이 딕셔너리의 형태로 입력 가능.
#forms.py

class CommentForm(forms.ModelForm):
    content = forms.CharField(label='', widget=forms.TextInput({attrs={'class':'form-control', 'placeholder': '댓글을 작성하세요.'}}))
    class Meta:
        model = Comment
        fields = ['content',]
  • 아래와 같은 방법으로도 작성 가능함
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['content']
        widgets = {
            'content': forms.TextInput(attrs={'placeholder':'댓글을 입력하세요'})
        }

댓글 생성 폼에 bootstrap 입히기 (_post.html)

{% if user.is_authenticated %} 
<form action="{% url 'posts:comment_create' post.id %}" method="POST">
    {% csrf_token %} 
    <div class="input-group">
        {{ comment_form }} 
        <div class="input-group-append">
            <input type="submit" value="Submit" class="btn btn-primary"/>
        </div>
    </div>
</form>
{% endif %}