본문 바로가기

매일 TIL

[내일배움캠프 8-3일] Django MTV(RUD), Django Form, URL Namespace

Redirect

어떠한 작업이 끝나고 내가 지정한 url로 돌려보내는 것.

from django.shortcuts import render, redirect
from .models import Article

redirect는 django.shortcut 안에 있음.

def create(request):
    title = request.POST.get('title')
    content = request.POST.get('content')
    # 새로운 Article 저장
    Article.objects.create(title=title, content=content)
    return redirect('articles')

create 뷰에서 return 부분을 수정해주면 된다.

create.html에서 요청을 render해서 보여주는 것이 아니라, redirect('경로 별명')을 통해 articles/ 페이지로 돌려보내게 됨.

더이상 create.html도 필요없게 되는 것.


새 글을 생성하고 나서 해당 글의 디테일 페이지로 가도록 해보자.

즉 articles/new/ 에서 제목과 내용을 입력하면 해당 디테일 페이지로 가도록.

def create(request):
    title = request.POST.get('title')
    content = request.POST.get('content')
    # 새로운 Article 저장
    Article.objects.create(title=title, content=content)
    return redirect('article_detail')

쉬운데? redirect 부분만 'article_detail'로 바꿔주면 되는거 아닌가? -> 아님

오류가 나는 이유 : 디테일 페이지로 가기 위해서는 해당 글의 id, 즉 pk값이 필요하다.

def create(request):
    title = request.POST.get('title')
    content = request.POST.get('content')
    # 새로운 Article 저장
    article = Article.objects.create(title=title, content=content)
    return redirect('article_detail', article.pk)

aritcle.pk라는 변수를 받아와야 함.

그러기 위해서 article = Article.~~~ 형태로 변수선언도 해주어야 한다.


Delete

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

urls에 delete를 추가.

이때 삭제되는 페이지는 pk, 즉 id를 가진 디테일 페이지이기 때문에 삭제할 때도 이 pk 값이 필요.

def delete(request, pk):
    article = Article.objects.get(pk=pk)
    article.delete()
    return redirect('articles')

delete 뷰 추가.

pk가 넘어오기 때문에 파라미터 추가해주고, 삭제할 하나의 디테일 페이지를 get을 통해 가져오면 된다.

해당 페이지를 삭제하고 다시 목록 페이지(articles)로 redirect 해주면 끝.

{% extends 'base.html' %}

{% block content %}

    <h2>글 상세 페이지</h2>
    <p>제목: {{ article.title }}</p>
    <p>내용: {{ article.content }}</p>
    <p>작성일시: {{ article.created_at }}</p>
    <p>수정일시: {{ article.updated_at }}</p>

    <a href="{% url 'articles' %}">목록 보기</a>

    <form action="{% url 'delete' article.pk %}" method="POST">
        {% csrf_token %}
        <input type="submit" value="삭제">
    </form>

{% endblock content %}

삭제 버튼은 각 디테일 페이지 내부에 위치하도록 설계.(article_detail.html)

DB에 있는 정보를 삭제하는 방식은 POST를 사용해야 한다. -> 즉 form으로 작성해야 함.

POST 이기 때문에 csrf 토큰처리 필수

 

문제 발생

그런데 url에 직접 articles/9/delete/ 를 입력하게 되면 자동으로 9번 디테일 페이지가 삭제되게 된다.
만약 delete 할 페이지를 우선 조회하고 싶다면?
즉 articles/9/delete/ 로 가더라도 디테일 페이지가 먼저 보이고, 이후 버튼으로 삭제하고 싶은 것.

 

그렇다면 POST 방식일 때는 삭제하고, GET 방식일 때는 삭제하지 않고 조회되도록 변경하면 되겠다.

def delete(request, pk):
    if request.method == 'POST':    
        article = Article.objects.get(pk=pk)
        article.delete()
        return redirect('articles')
    return redirect('article_detail', pk)

조건문 처리 완료.


Update

우선 수정 페이지 먼저 구현

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

urls에 edit 추가.

def edit(request, pk):
    article = Article.objects.get(pk=pk)
    context = {'article': article}
    return render(request, 'edit.html', context)

edit 뷰 작성.

역시 pk 받아오는 것 동일하고, 수정 페이지인 edit.html이 필요.

{% extends 'base.html' %}

{% block content %}
    <h1>Edit Article</h1>

    <form action="" method="POST">
        {% csrf_token %}
        <lable for="title">제목</lable>
        <input type="text" name="title" id="title" value="{{ article.title }}">
        <br><br>
       
        <lable for="">내용</lable>
        <textarea name="content", id="content", cols="30", rows="10">{{ article.content }}</textarea><br><br>

        <button type="submit">수정</button>
    </form>

    <a href="{% url 'article_detail' article.pk %}">이전으로</a>
{% endblock content %}

edit.html, 사실 수정과 생성은 형식이 비슷하기 때문에 new.html을 참조하면 됨.

여기서 html 문법이 조금 나온다.

수정한다는 것은 우리가 new에서 작성한 title과 content가 이미 페이지에 입력되어 있어야 한다는 것.

이때 input 태그에서는 value를 사용한다. value="{{ article.title }}"을 사용함으로써 제목을 입력해 놓을 수 있음

textarea는 조금 다름. 태그 안쪽에 {{ article.content }}를 입력해줘야 함.

 

이제 수정 버튼을 눌렀을때 수정될 수 있도록 update 수정 로직을 추가

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

우선 urls에 update 추가.

def update(request, pk):
    title = request.POST.get('title')
    content = request.POST.get('content')

    article = Article.objects.get(pk=pk)
    article.title = title
    article.content = content
    article.save()

    return redirect('article_detail', article.pk)

update 뷰 작성.

POST로 들어온 요청으로부터 title, content를 가져옴.

해당 pk의 article 데이터베이스에서 조회.

들어온 title, content를 넣어준 뒤 저장해주는 기능.

{% extends 'base.html' %}

{% block content %}
<h1>Edit Article</h1>

<form action="{% url 'update' article.pk %}" method="POST">
    {% csrf_token %}
    <lable for="title">제목</lable>
    <input type="text" name="title" id="title" value="{{ article.title }}">
    <br><br>

    <lable for="">내용</lable>
    <textarea name="content" , id="content" , cols="30" , rows="10">{{ article.content }}</textarea><br><br>

    <button type="submit">수정</button>
</form>

<a href="{% url 'article_detail' article.pk %}">이전으로</a>
{% endblock content %}

edit.html

이제 action에 update 로직을 추가.

{% extends 'base.html' %}

{% block content %}

    <h2>글 상세 페이지</h2>
    <p>제목: {{ article.title }}</p>
    <p>내용: {{ article.content }}</p>
    <p>작성일시: {{ article.created_at }}</p>
    <p>수정일시: {{ article.updated_at }}</p>

    <a href="{% url 'articles' %}">목록 보기</a>
    <a href="{% url 'edit' article.pk %}">
        <button>수정하기</button>
    </a>

    <form action="{% url 'delete' article.pk %}" method="POST">
        {% csrf_token %}
        <input type="submit" value="삭제">
    </form>

   

{% endblock content %}

article_detail.html

수정 버튼을 추가, 누르면 edit으로 갈 수 있도록 a태그 생성.

수정은 보여주는 것이기 때문에 POST가 아닌 GET, 그렇기 때문에 form으로 작성하지 않는다.


Django Form

  • 내가 직접 커스텀할 코드가 많다 -> html form
  • 그렇지 않다면 -> django form

그럼 선언해보자

우선 form.py 파일을 articles 앱 안쪽에 생성한 뒤,

from django import forms

class ArticleForm(forms.Form):
    title = forms.CharField(max_length=50)
    content = forms.CharField()

이런 식으로 form에서 필요로 하는 input들을 정의해주면 됨.(필드 정의, 필드는 공식문서 참고)

모델과 굉장히 유사한 것을 알 수 있다.


이제 form을 적용해보자

새 글 작성에 적용해봄(new.html)

{% extends 'base.html' %}

{% block content %}
    <h1>New Article</h1>

    <form action="{% url 'create' %}" method="POST">
        {% csrf_token %}
        <lable for="title">제목</lable>
        <input type="text" name="title" id="title"><br><br>
       
        <lable for="">내용</lable>
        <textarea name="content", id="content", cols="30", rows="10"></textarea><br><br>

        <button type="submit">저장</button>
    </form>

    <a href="{% url 'articles' %}">목록으로</a>
{% endblock content %}

기존 html form으로 작성된 new.html

{% extends 'base.html' %}

{% block content %}
    <h1>New Article</h1>

    <form action="{% url 'create' %}" method="POST">
        {% csrf_token %}

        {{ forms.as_p }}

        <button type="submit">저장</button>
    </form>

    <a href="{% url 'articles' %}">목록으로</a>
{% endblock content %}

django form을 적용시킨 new.html

여기서 as_p는 p 태그로 감싼다는 의미.

다만 중요한 점은 form 태그는 직접 작성해야 한다는 것.

어디로 데이터를 보낼지, 어떤 메서드를 사용하는지에 대해서는 직접 써줘야 한다.

 

*여기서 forms는 context로 넘어온 것, 즉 views에서 forms를 넘겨준다는 것.

 

그렇다면 views를 바꿔보자

from django.shortcuts import render, redirect
from .models import Article
from .forms import ArticleForm

우선 forms에서 ArticleForm을 import.

def new(request):
    forms = ArticleForm()
    context = {'forms': forms}
    return render(request, 'new.html', context)

forms변수 할당 후, 역시 이것도 context를 통해 넘겨준다.


Form Widget

페이지를 보면 내용 칸이 너무 작다, 예전처럼 크게 바꿀 수 없을까? -> widget을 사용한다.

input이 다른 형태로 보여지도록 조절하는 것이 바로 widget.

from django import forms

class ArticleForm(forms.Form):
    title = forms.CharField(max_length=50)
    content = forms.CharField(widget=forms.Textarea)

괄호 안에 widget = forms.필요한 형식

widget 종류 역시 공식문서 참고하면서 필요할 때 쓰면 된다.

 

장르도 선택할 수 있도록 해보자

새 article을 생성할 때(articles/new/) 장르를 선택하는 기능을 추가해보자

from django import forms

class ArticleForm(forms.Form):
    GENRE_CHOICES = [
        ('technology', 'Technology'),
        ('life', 'Life'),
        ('hobby', 'Hobby'),
    ]

    title = forms.CharField(max_length=50)
    content = forms.CharField(widget=forms.Textarea)
    genre = forms.ChoiceField(choices=GENRE_CHOICES)

우선 GENRE_CHOICES라는 리스트를 정의해 놓아야 함.

여기서 요소는 (A, B)형태로 저장된다.

     A : 실제로 DB에 저장될 값

     B : 사용자에게 보여지는 형식

이후 genre 변수에 새로운 필드를 할당하면 끝.

이 부분은 구현이 아닌 그냥 알아보는 부분이라고 생각하고 넘어가자.(더 구현하면 너무 길어진다고 하심)


Django Model Form

form과 model이 너무 비슷한데... 그냥 model 참조해서 알아서 만들면 안될까?

-> 됨.

from django import forms
from .models import Article

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

forms.py

어떤 model을 참조할지 Meta 클래스에 적어주면 된다.

어떤 필드를 가져올지, 여기서는 전부.

제외할 필드는? exclude로 제외시켜주면 됨.

 

이제는 뷰에서 Model Form을 사용해보자

def create(request):
    form = ArticleForm(request.POST)
    if form.is_valid():
        article = form.save()
        return redirect('article_detail', article.pk)
    return redirect('new')

request.POST를 통해 넘어온 데이터로 바인딩된 form을 만들어줌.

그 form에 입력된 데이터가 valid 한지 확인, 그렇다면 DB에 저장.

그걸 article 변수로 받아서 그 페이지의 pk를 넘겨준 뒤, 해당 pk의 디테일 페이지로 redirect 한 것.

 

new와 create 뷰가 흡사한데? -> GET일 때와 POST일때 나눠서 하나의 뷰로 처리해보자

def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save()
            return redirect('article_detail', article.pk)
    else:
        form = ArticleForm()
   
    context = {'form': form}
    return render(request, 'create.html', context)

이런식으로 합칠 수 있음. 더이상 new 뷰는 없다. new 뷰의 기능을 else 문으로 처리한 것.

(참고로 urls에서도 당연히 new path 지워줘야 함)

create로 들어오는 요청이 GET인 경우, else문 실행.

즉 빈 form을 만들고 context에 담아 전달.

{% extends 'base.html' %}

{% block content %}
    <h1>New Article</h1>

    <form action="{% url 'create' %}" method="POST">
        {% csrf_token %}

        {{ form.as_p }}

        <button type="submit">저장</button>
    </form>

    <a href="{% url 'articles' %}">목록으로</a>
{% endblock content %}

create.html

만약 create 페이지에서 저장 버튼을 누르면, POST 방식으로 다시 create로 들어가게 됨.

POST 방식인 경우 위에서 했던 것처럼 바인딩된 form을 만들고, valid한지 확인하고, 저장하고, redirect 까지 진행.

 

edit과 update도 합쳐보자

Model Form은

     - instance라는 값이 들어오지 않으면 -> 새로 데이터를 생성.

     - instance에 article 객체가 들어오면 이 article을 수정.

def update(request, pk):
    article = Article.objects.get(pk=pk)
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            article = form.save()
            return redirect('article_detail', article.pk)
       
    else:
        form = ArticleForm(instance=article)

        context = {
            'form': form,
            'article': article,
            }
        return render(request, 'update.html', context)

"수정하기" 버튼 ("수정" 버튼 아님) 을 누르면 -> GET 요청

우선 article 조회 후 else문 수행.

article을 넣은 form을 만들고, context에 담아 article과 함께 update.html로 보냄.

{% extends 'base.html' %}

{% block content %}
<h2>Edit Article</h2>

<form action="{% url 'update' article.pk %}" method="POST">
    {% csrf_token %}

    {{ form.as_p }}

    <button type="submit">수정</button>
</form>

<a href="{% url 'article_detail' article.pk %}">이전으로</a>
{% endblock content %}

update.html

articles/16/update/ 페이지에서 실제로 수정 처리를 거치고 "수정" 버튼을 누르면 -> POST 요청

POST로 넘어온 데이터를 기존 instance에 넣어주고,

들어온 데이터가 valid 라면, form.save()로 수정.

이후 article 수정된 것 넘어오면 해당 pk의 디테일 페이지로 redirect.


Namespace

서로 다른 앱인데 url name, 즉 이름(별명)이 같을 수 있다. 이러한 경우 namespace를 사용하면 됨

namespace를 사용하여 url을 따로 관리한다는 개념.

url을 내가 사용한다면, 먼저 namespace를 확인하고, 이후 url을 확인하는 방법.

from django.urls import path
from . import views

app_name = 'articles'

urlpatterns = [
    path("", views.articles, name='articles'),
    path('create/', views.create, name='create'),
    path('<int:pk>/', views.article_detail, name='article_detail'),
    path('<int:pk>/delete/', views.delete, name='delete'),
    path('<int:pk>/update/', views.update, name='update'),

    path('data-throw/', views.data_throw, name='data-throw'),
    path('data-catch/', views.data_catch, name='data-catch'),
]

articles앱 의 urls.py

app_name = 'articles' 이 한 문장만 추가해주면 끝.

사용할 때는 articles: 만 추가하면 된다.

    <a href="{% url 'create' %}">

사용 안한 버전

    <a href="{% url 'articles:create' %}">

사용 한 버전

 

우리가 배운 내용 중에서는

  • html에서 태그 형태로 url 지정할 때
  • redirect 할 떄

이 두 경우에서 namespace를 사용해주면 된다.


Templates Namespace

원래 우리가 사용하던 구조.

장고에서 권장하는 구조.

articles 앱의 templates 안에 또 articles 폴더를 만들고.

그 폴더 안에 템플릿들을 저장하는 구조.

def index(request):
    return render(request, 'index.html')

이것 역시

def index(request):
    return render(request, 'articles/index.html')

이렇게 경로를 바꿔줘야 함.


오늘의 회고

회고할 시간도 힘도 없다...

목표는 전부 완료.

 

내일의 목표는 1-18 듣기, Git 복습