Python/Django

Django에서 blog 구현

noodle-dev 2020. 2. 19. 09:55

1:N 관계를 ORM에서 어떻게 표현하며 일반 DB와 어떻게 연결되는지 살펴보자.

새로운 프로젝트 생성

  • 프로젝트 생성
python manage.py startapp blog                #플젝생성
  • settings.py 에 blog 라는 앱을 설치 (평소엔 꼭 설치할 필요는 없지만, DB와 연동하려면 설치 필요)
INSTALLED_APPS = [..., 'blog',]
  • blog/models.py - 테이블 생성
from django.db import models
from django.utils import timezone

class Post(models.Model):
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    text = models.TextField()  # 글자수에 제한 없는 텍스트
    created_date = models.DateTimeField(
        default=timezone.now)  # 날짜와 시간
    published_date = models.DateTimeField(
        blank=True, null=True) #  필드가 폼에서 빈 채로 저장되는 것을 허용

    def publish(self):
        self.published_date = timezone.now()        #수정된 날짜
        self.save()

    def __str__(self):      #print ftn에 적용할 str ftn
        return self.title

auth.User : system table. 시스템이 기본으로 생성한 table

on_delete=models.CASCADE : cascade:종속성. user 지울 때 관련된 post 자동으로 지워라.

blank=True : form에서 빈 데이터 허용 (application level)

null=True : 필드값 optional (database level)

blank=False, null=True : application에서는 값 필수, db차원에서는 값 optional

실제로 blog_post에 생성된 테이블을 보면, id, title, text, created_date, published_date, author_id

기본적으로 class 만들면 기본적으로 id field는 자동으로 만들어진다. (django 규칙)

보통 id값은 auto-increment type 으로 생성된다.

author로 테이블을 만들었는데 author_id로 만들어졌다. 기본적으로 author에도 id field가 있는 것이다.

즉 외래키로 잡힌 것은 underbar 붙여 id 생성된다.

  • blog/admin.py - models에서 만든 table 등록
from blog.models import Post    #루트폴더/blog/models.py 안의 Post Class를 참조
admin.site.register(Post)
  • migration
python manage.py makemigrations        #변경사항 기록
python manage.py migrate            #실제 데이터베이스 변경사항 반영

Applying ... OK 떠야 반영 성공된 것이다.

localhost:8000/admin 에서 확인해보자.

auth.User : 시스템 테이블로, admin 화면에서 AUTHENTICATION AND AUTHORIZATION > Users 테이블을 가리킨다.

parameter 전달하는 방법에 차이가 있는 것일 뿐

GET method : 전통적인 방식

정적 URL

동적 URL pattern : 프로그램 전달 방식???

DB Browser에서 blog_post 테이블의 스키마를 보면 REFERENCES "auth_user" ("id") 로 되어있고,

시스템 테이블인 auth_user 테이블 밑의 id는 integer이다.

DB Browser에서는 신경 안 쓰고 작업해도 되지만, 코딩 시에는 auth_user의 'id'를 참고한다는 것을 고려해야 한다.

in Jupyter Notebook for unit test

Post Record 생성

from blog.models import Post
p = Post(title="오늘 점심 메뉴", text="뭐지?")
p.save()
#result
#IntegrityError: NOT NULL constraint failed: blog_post.author_id

로 하면, NOT NULL field에 값을 채워주지 않았기 때문에 오류난다.

from blog.models import Post    #객체 참조
from django.contrib.auth.models import User

u = User.objects.all().get(username='lee')  #system에서 만든 User table
#객체생성방법: 하나는 객체 만들어서 save, 두번째는 그냥 함수 사용
p = Post(title="오늘 점심 메뉴", text="뭐지?", author=u)
p.save()
p.title = "오늘 저엄심 메뉴"
p.save()    #p는 아직 기존 레코드를 가리키고 있으므로 update된다.

in APP

동적 URL 경로 구성

  • mysite/urls.py
urlpatterns = [
    path('blog/', include('blog.urls')),
    ...
]

blog폴더 내의 urls.py를 불러오고, 기본 경로는 blog/ 로 시작한다.

  • blog/urls.py
urlpatterns = [
    path('<name>/', views.index2),      #<name> parameter에 대해 동적으로 mapping
    path('<int:pk>/detail', views.index3),
]

으로 사용하여, 동적으로 URL을 mappping할 수 있다.

pk라는 파라미터를 앞에 int:를 붙여서 변수 형태를 integer로 한정시킬 수 있다.

  • blog/views.py
def index2(request, name):
    return HttpResponse("INDEX2 OK" + name)
def index3(request, name):
    return HttpResponse("INDEX3 OK" + str(pk))

404 Error Exception

  • blog/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from blog.models import Post

def index3(request, pk):
    #p = Post.objects.get(pk=pk)
    p = get_object_or_404(Post, pk=pk)      #pagenotfound(404) exception return
    return HttpResponse("INDEX3 OK" + p.title)

Post.objects.get(pk=pk) 에서 왼쪽의 pk는 parameter name, 오른쪽의 pk는 variable이다.

get 함수는 pk=pk인 것을 찾아주는 함수이다.

get_object_or_404(Post, pk=pk) : pk=pk인 것을 찾고, 에러 시 아랫줄 return이 아니라 PageNotFound(404) error로 리턴시키도록 exception을 발생시킨다.

List template

  • blog/urls.py
urlpatterns = [
    ...
    path('list', views.list),
]
  • blog/views.py
def list(request):
    data = Post.objects.all()
    return render(request, "blog/list.html", {"data":data})

header/footer contents

  • templates/blog/base.html
{% block content %}
{% endblock %}

부모에게 상속을 받고, 내가 렌더링한 부분은 여기에 넣겠다.

  • templates/blog/list.html
{% extends 'blog/base.html' %}


{% block content %}
내용
{% endblock %}

{% extends 'blog/base.html' %} 를 base template으로 가져올 건데,

{% block content %} 부터 {% endblock %} 까지 렌더링한다.

LAST CODE

  • blog/urls.py
from django.urls import path
from . import views     #from 다음에는 폴더명. import 다음에는 함수명이나

urlpatterns = [
    path('', views.index),
    path('<name>/', views.index2),      #<name> parameter에 대해 동적으로 mapping

    path('list', views.list),
    path('<int:pk>/detail', views.detail),
]
  • blog/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from blog.models import Post

# Create your views here.

def index(request):
    return HttpResponse("INDEX. OKOKOK")

def index2(request, name):
    return HttpResponse("INDEX2 OK" + name)

def list(request):
    data = Post.objects.all()
    return render(request, "blog/list.html", {"data":data})

def detail(request, pk):
    p = get_object_or_404(Post, pk=pk)      #pagenotfound(404) exception return
    return render(request, "blog/detail.html", {"d":p})
  • templates/blog/base.html
<h1><font color="red">My Blog</font></h1>


<!--부모에게 상속을 받고, 내가 렌더링한 부분은 여기에 넣겠다.-->
{% block content %}
{% endblock %}


<br><br><br>
copy right........<br>
서울특별시........
  • templates/blog/list.html
{% extends 'blog/base.html' %}
{% block content %}

    {% for d in data %}
        <a href="{{d.pk}}/detail">{{d.title}}</a>
        <hr>
    {% endfor %}

{% endblock %}
  • templates/blog/detail.html
{% extends 'blog/base.html' %}
{% block content %}

    <h2>게시물 보기</h2>
    title >> {{d.title}}<hr>
    {{d.text|linebreaks}}
    <br><a href="/blog/list">뒤로가기</a>

{% endblock %}

{{변수|linebreaks}} 옵션을 주면 엔터 반영되어 렌더링된다.

in APP - Class type View 적용

class와 객체 이용한 일반적인 로그인 폼

클래스형 뷰 이론

목적: GET method와 POST method를 다른 페이지로 적용할 때, 편리를 위해 Class type View를 사용한다.

  • 클래스로 작성되어 있는 뷰 객체를 말한다.
  • 상속과 믹스인 기능 사용으로 코드의 재사용이 가능
  • 뷰의 체계적 관리
  • 제네릭 뷰 작성
#urls.py
urlpatterns = [
    path('login/', views.LoginView.as_view())
]

클래스형 뷰는 클래스로 진입하기 위한 진입 메소드를 제공하는데, 이것이 위의 as_view()메소드이며, 아래의 순서로 요청을 처리한다.

  • as_view() 메소드에서 클래스의 인스턴스를 생성한다.
  • 생성된 인스턴스의 dispatch() 메소드를 호출한다.
  • dispatch() 메소드는 요청을 검사해서 HTTP의 메소드(GET, POST)를 알아낸다.
  • 인스턴스 내에 해당 이름을 갖는 메소드로 요청을 중계한다.
  • 해당 메소드가 정의되어 있지 않으면, HttpResponseNotAllowd 예외를 발생시킨다.
#views.py
from django.views.generic import View

class PostView(View):
    def get(self, request):
        return HttpResponse("get OK")
    def post(self, request):
        return HttpResponse("post OK")
  • PostView 클래스는 View 클래스를 상속받는다.
  • View 클래스에는 as_view() 메소드와 dispatch() 메소드가 정의되어 있다.

함수형 뷰와 비교했을 때 클래스형 뷰가 가지는 장점

  • GET, POST 등의 HTTP 메소드에 따른 처리를 메소드명으로 구분 할 수 있어, 좀 더 깔끔한 구조의(IF 문이 없는) 코드를 생산할 수 있다.
  • 다중 상속과 같은 객체 지향 기술이 가능하여 코드의 재사용성이나 개발 생산성을 높여준다.
#함수형 뷰에서의 메소드 구분
def my_view(request):  
    if request.method == 'GET':
        # 뷰 로직 작성
        return HttpResponse('result')

함수형 뷰에서는 예제에서 볼 수 있듯 HTTP 메소드별 다른 처리가 필요할 때 if 문을 이용해야 한다. 하지만, 클래스형 뷰는 다음과 같이 코드의 구조가 훨씬 깔끔해진다.

#클래스형 뷰에서의 메소드 구분
from django.views.generic import View

class PostView(View):
    def get(self, request):
        return HttpResponse("get OK")
    def post(self, request):
        return HttpResponse("post OK")

클래스형 뷰에서는 HTTP 메소드 이름으로 클래스 내의 메소드를 정의하면 된다.
단, 메소드명은 소문자로~ 이러한 처리가 가능한 것은 내부적으로 dispatch() 메소드가 어떤 HTTP 메소드로 요청되었는지 알아내고, 이를 처리해주기 때문이다.

상속 기능 가능

개발자가 작성하는 대부분의 클래스형 뷰는 장고가 제공해주는 제네릭 뷰를 상속받아 작성한다.

제네릭 뷰: 뷰 개발 과정에서 공통적으로 사용할 수 있는 기능들을 추상화하고, 장고에서 기본적으로 제공해주는 클래스형 뷰

위에서의 View는 특별한 로직이 없고, URL 맞춰 해당 템플릿 파일의 내용만 보여줄 때 사용하는 제네릭 뷰이기 때문에 위처럼 상속받아 불러오는 것만으로도 사용할 수 있는 것이다.

Django 의 제네릭 뷰

Django 에서 제공하는 제네릭 뷰는 다음과 같이 4가지로 분류할 수 있다.

  • Base View: 뷰 클래스를 생성하고, 다른 제네릭 뷰의 부모 클래스를 제공하는 기본 제네릭 뷰
  • Generic Display View: 객체의 리스트를 보여주거나, 특정 객체의 상세 정보를 보여준다.
  • Generic Edit View: 폼을 통해 객체를 생성, 수정, 삭제하는 기능을 제공한다.
  • Generic Date View: 날짜 기반 객체의 년/월/일 페이지로 구분해서 보여준다.

아래는 위 4가지 분류에 따른 구체 뷰 클래스에 대한 설명이다.

  • Base View
    • View: 가장 기본이 되는 최상위 제네릭 뷰
    • TemplateView: 템플릿이 주어지면 해당 템플릿을 렌더링한다.
    • RedirectView: URL이 주어지면 해당 URL로 리다이렉트 시켜준다.
  • Generic Display View
    • DetailView: 객체 하나에 대한 상세한 정보를 보여준다.
    • ListView: 조건에 맞는 여러 개의 객체를 보여준다.
  • Generic Edit View
    • FormView: 폼이 주어지면 해당 폼을 보여준다.
    • CreateView: 객체를 생성하는 폼을 보여준다.
    • UpdateView: 기존 객체를 수정하는 폼을 보여준다.
    • DeleteView: 기존 객체를 삭제하는 폼을 보여준다.
  • Generic Date View
    • YearArchiveView: 년도가 주어지면 그 년도에 해당하는 객체를 보여준다.
    • MonthArchiveView: 월이 주어지면 그 월에 해당하는 객체를 보여준다.
    • DayArchiveView: 날짜가 주어지면 그 날짜에 해당하는 객체를 보여준다.

제네릭 뷰의 전체 리스트는 여기에서 확인 가능하다.

클래스형 뷰에서의 폼 처리

  • blog/views.py
from django.forms import Form, CharField, Textarea

class PostForm(Form):
    title = CharField(label='제목', max_length=20)
    text = CharField(label='내용', widget=Textarea)

class PostEditView(View):
    def get(self, request, pk):     #특정 포스트를 수정하므로 pk parameter를 받아와야 한다.
        #초기값 지정
        post = get_object_or_404(Post, pk=pk)
        form = PostForm(initial={'title':post.title, 'text':post.text})
        return render(request, "blog/edit.html", {'form':form})

    def post(self, request, pk):
        form = PostForm(request.POST)
        post = get_object_or_404(Post, pk=pk)
        post.title = form['title'].value()
        post.text = form['text'].value()
        post.publish()
        return redirect('list')

initial={dictionary}

  • templates/blog/edit.html
{{ form.as_p }}
  • form_class: 사용자에 보여줄 폼을 정의한 forms.py 파일 내의 클래스명
  • template_name: 폼을 포함하여 렌더링할 템플릿 파일 이름
  • success_url: MyFormView 처리가 정상적으로 완료되었을 때 리다이렉트 될 URL
  • form_valid() 함수: 유효한 폼 데이터로 처리할 로직 코딩. 반드시 super() 함수를 호출해야 함.

참고: http://ruaa.me/django-view/

  • 폼 기능을 추가한 blog/views.py
from django.forms import Form, CharField, Textarea, ValidationError

def validator(value):
    if len(value) < 5 : raise ValidationError("길이가 너무 짧아요");

class PostForm(Form):
    title = CharField(label='제목', max_length=20, validators=[validator])
    text = CharField(label='내용', widget=Textarea)

class PostEditView(View):
    def get(self, request, pk):     #특정 포스트를 수정하므로 pk parameter를 받아와야 한다.
        #초기값 지정
        post = get_object_or_404(Post, pk=pk)
        form = PostForm(initial={'title':post.title, 'text':post.text})
        return render(request, "blog/edit.html", {'form':form, 'pk':pk})

    def post(self, request, pk):
        form = PostForm(request.POST)
        if form.is_valid():
            post = get_object_or_404(Post, pk=pk)
            post.title = form['title'].value()
            post.text = form['text'].value()
            post.publish()
            return redirect('list')
        return render(request, 'blog/edit.html', {'form':form, 'pk':pk})

Authentication

global settings에 정의된 인증관련 기본 설정에 AUTH_USER_MODEL = 'auth.User' 로 정의되어있다.

#in Jupyter Notebook for Unit Test
from django.contrib.auth import authenticate
user = authenticate(username = 'home', password='choikt1234')
if user == None : print(user)
  from django.contrib.auth import authenticate

user = authenticate(username = 'home', password='choikt1234')

if user == None : print(user)        #None

User 모델 클래스 획득 방법

  • 직접 User 모델 import (비추)
from django.contrib.auth.models import User
User.objects.all()

global settings 오버라이딩을 통해서 인증 User 모델을 다른 모델로 변경할 수 있음

  • get_user_model helper 함수를 통해 모델 클래스 참조 (추천)
from django.contrib.auth import get_user_model

User = get_user_model()
User.objects.all()
  • settings.AUTH_USER_MODEL 을 통한 모델클래스 참조 (추천)
from django.conf import settings # 추천!
from django.conf.auth.models import User # 비추
from django.db import models

class Post(models.Model):
    author = models.ForeignKey(User)         # 비추
    author = models.ForeignKey('auth.User') # 비추
    author = models.ForeignKey(settings.AUTH_USER_MODEL) # 추천!

view에서 현재 로그인 유저 획득하는 방법

  1. FBV : request.user
  2. CBV : self.request.user
    • 로그인 상태 :settings.AUTH_USER_MODEL 클래스 인스턴스
    • 로그아웃 상태 :django.contrib.auth.models.AnonymousUser 클래스 (모델 인스턴스가 아님, 다른 모델과 관계 불가능)
    • context_processor를 통해서 user가 모든 view에 context로 기본 제공 됨

출처: https://wayhome25.github.io/django/2017/05/18/django-auth/

경로 찾아주는 template 명령어

views 사용때문에 생기는 상대경로 문제를 해결해주는 방법이다.

html에 다음과 같은 함수로 상대경로 문제를 해결할 수 있다.

{% url 'url_name' param1 param2 param3 %}

  • blog/urls.py
urlpatterns = [
    path('', views.index),
    path('list', views.list, name='list'),
    path('<int:pk>/detail', views.detail, name='detail'),       #function base

    path('list2', views.PostView.as_view()),                    #class base
    path('login/', views.LoginView.as_view(), name='login'),
]

path에 name을 정의하게되면, name으로 경로명을 자동으로 알아낼 수 있다.

  • blog/views.py
...
            return redirect('login')    #urls.py에서 지정한 name. NOT 경로명.
...
        return redirect('list')

urls.py에서 login이라는 name을 지정했으므로, 경로명이 아니라 name을 불러온 것이다.

  • templates/blog/login.html
<form action="{% url 'login' %}" method="post">

{% url 'login' %} : url 명령어 뒤 name을 알아서 절대경로를 찾아 지정해준다.

  • templates/blog/list.html
    {% for d in data %}
        <a href="{% url 'detail' d.pk %}">{{d.title}}</a>
        <hr>
    {% endfor %}

{% url 'detail' d.pk %} : url 명령어 뒤 name=detail, parameter=d.pk 를 가져와 절대경로를 찾아 지정해준다.

cf) in templates,

명령어 사용할 때에는 {% 명령어%}

변수값을 가져올 때에는 {{변수명}}

여러 가지 경로 지정 방법 비교

  • CODE of list.html
{% extends 'blog/base.html' %}
{% block content %}

    {% for d in data %}
        <a href="{% url 'detail' d.pk %}">{{d.title}}</a><br>
        <a href="/blog/{{d.pk}}/detail">{{d.title}}</a><br>
        <a href="/blog/detail?id={{d.pk}}">{{d.title}}</a><br>
        <a href="detail?id={{d.pk}}">{{d.title}}</a><br>
        <hr>
    {% endfor %}

{% endblock %}

첫번째 방법: 경로 찾아주는 template 명령어

두번째 방법: 절대경로

세번째: get parameter로 받아올 경우, 절대경로

네번째: get parameter로 받아올 경우, 상대경로

  • print
<a href="/blog/1/detail">오늘 날씨가 추워요</a><br>
<a href="/blog/1/detail">오늘 날씨가 추워요</a><br>
<a href="/blog/detail?id=1">오늘 날씨가 추워요</a><br>
<a href="detail?id=1">오늘 날씨가 추워요</a><br>

LAST CODE

blog/models.py
from django.db import models


class User(models.Model) :
    userid = models.CharField(max_length=10, primary_key=True)
    name = models.CharField(max_length=10)
    age = models.IntegerField()
    hobby = models.CharField(max_length=20)

    def __str__(self):                          #print 적용할 때 자동으로 적용되는 함수
        return f"{self.userid} / {self.name} / {self.age}"
blog/urls.py
from django.urls import path
from . import views     #from 다음에는 폴더명. import 다음에는 함수명이나

urlpatterns = [
    path('', views.index),
#    path('<name>/', views.index2),      #<name> parameter에 대해 동적으로 mapping
#    path('<int:pk>/detail', views.index3),

    path('login/', views.LoginView.as_view(), name='login'),    # class base

    path('list/', views.list, name='list'),
    path('<int:pk>/detail/', views.detail, name='detail'),       #function base
    path('add/', views.PostView.as_view(), name='add'),
    path('<int:pk>/edit/', views.PostEditView.as_view(), name='edit'),
]

login

  • blog/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse
from blog.models import Post
from django.views.generic import View
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.forms import Form, CharField, Textarea, ValidationError

class LoginView(View):
    def get(self, request):
        return render(request, "blog/login.html")

    def post(self, request):
        #Loging 처리
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = authenticate(username=username, password=password)
        if user == None :
            return redirect('login')    #urls.py에서 지정한 name. NOT 경로명.

        #로그인 성공한 경우
        request.session['username'] = username
        return redirect('list')
  • templates/blog/login.html
<form action="{% url 'login' %}" method="post">    <!--action 빼면 자기 자신, url이라는 명령어 뒤 name-->
{% csrf_token %}
    username <input type="text" name="username"><br>
    password <input type="password" name="password"><br>
    <input type="submit" value="로그인">
</form>

base.html

<h1><font color="red">My Blog</font></h1>
로그인 사용자: {{username}} 님<br>

<!--부모에게 상속을 받고, 내가 렌더링한 부분은 여기에 넣겠다.-->
{% block content %}
{% endblock %}


<br><br><br>
copy right........<br>
서울특별시........

list

  • blog/views.py
def list(request):
    username = request.session['username']              #text
    user = User.objects.get(username=username)          #object
    data = Post.objects.all().filter(author=user)
    context = {"data":data, 'username':username}
    return render(request, "blog/list.html", context)
  • templates/blog/list.html
{% extends 'blog/base.html' %}
{% block content %}
    <a href="{% url 'add' %}">글쓰기</a><br>
    <h2>글 리스트</h2>
    {% for d in data %}
        <a href="{% url 'detail' d.pk %}">{{d.title}}</a><br>
        <hr>
    {% endfor %}
{% endblock %}

detail

  • blog/views.py
def detail(request, pk):
    p = get_object_or_404(Post, pk=pk)      #에러나면 아래 return이 아닌, pagenotfound(404) exception로 리턴시킨다.
    return render(request, "blog/detail.html", {"d":p})
  • templates/blog/detail.html
{% extends 'blog/base.html' %}
{% block content %}

    <h2>게시물 보기</h2>
    {{d.title}}<hr>
    {{d.text|linebreaks}}
    <br><a href="{% url 'edit' d.pk %}">수정</a>
    <br><a href="/blog/list">목록 보기</a>

{% endblock %}

add

  • blog/views.py
class PostView(View):
    def get(self, request):
        username = request.session['username']
        return render(request, "blog/add.html", {'username':username})

    def post(self, request):
        title = request.POST.get('title')
        text = request.POST.get('text')
        username = request.session['username']
        user = User.objects.get(username=username)
        Post.objects.create(title=title, text=text, author=user)    #create:생성과 동시에 save
        return redirect('list')
  • templates/blog/add.html
{% extends 'blog/base.html' %}
{% block content %}
    <form action="{% url 'add' %}" method="post">
    {% csrf_token %}
        제목  <input type="text" name="title" /><br>
        내용  <textarea rows="10" cols="30" name="text"></textarea>

        <input type="submit" value="작성">
    </form>
{% endblock %}

edit

데이터를 읽어와서 default value로 넣어줘야 한다.

  • blog/views.py
def validator(value):
    if len(value) < 5 : raise ValidationError("길이가 너무 짧아요");

class PostForm(Form):
    title = CharField(label='제목', max_length=20, validators=[validator])
    text = CharField(label='내용', widget=Textarea)

class PostEditView(View):
    def get(self, request, pk):     #특정 포스트를 수정하므로 pk parameter를 받아와야 한다.
        #초기값 지정
        post = get_object_or_404(Post, pk=pk)
        form = PostForm(initial={'title':post.title, 'text':post.text})
        return render(request, "blog/edit.html", {'form':form, 'pk':pk})

    def post(self, request, pk):
        form = PostForm(request.POST)
        if form.is_valid():
            post = get_object_or_404(Post, pk=pk)
            post.title = form['title'].value()
            post.text = form['text'].value()
            post.publish()
            return redirect('list')
        return render(request, 'blog/edit.html', {'form':form, 'pk':pk})
  • templates/blog/edit.html
{% extends 'blog/base.html' %}
{% block content %}
    <form action="{% url 'edit' pk %}" method="post">
    {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="작성">
    </form>
{% endblock %}