첫 Wagtail 사이트¶
이 튜토리얼에서는 Wagtail을 사용하여 블로그를 만드는 방법을 보여줍니다. 또한 튜토리얼을 통해 Wagtail의 일부 기능을 직접 체험할 수 있습니다.
이 튜토리얼을 완료하려면 기본적인 프로그래밍 지식과 웹 개발 개념에 대한 이해가 있는 것이 좋습니다. Python 및 Django 프레임워크에 대한 기본적인 이해가 있으면 이 튜토리얼을 더 깊이 이해하는 데 도움이 되지만 필수는 아닙니다.
참고
대신 기존 Django 프로젝트에 Wagtail을 추가하려면 Wagtail을 Django 프로젝트에 통합하기를 참조하십시오.
Wagtail 설치 및 실행¶
종속성 설치¶
Wagtail이 지원하는 호환되는 Python 버전을 확인하십시오.
적절한 버전의 Python 3이 있는지 확인하려면 다음 명령을 실행하십시오.
python --version
# 또는:
python3 --version
# **Windows에서** (cmd.exe, Windows용 Python 런처 사용):
py --version
앞의 명령 중 어느 것도 버전 번호를 반환하지 않거나 3.9보다 낮은 버전을 반환하는 경우 Python 3을 설치하십시오.
가상 환경 생성 및 활성화¶
이 튜토리얼에서는 설치된 종속성을 다른 프로젝트와 격리하는 가상 환경을 사용하는 것이 좋습니다.
이 튜토리얼에서는 Python 3과 함께 패키지된 venv를 사용합니다. Ubuntu에서는 sudo apt install python3-venv 를 실행하여 설치해야 할 수 있습니다.
Windows에서 (cmd.exe), 다음 명령을 실행하여 가상 환경을 만듭니다.
py -m venv mysite\env
다음을 사용하여 이 가상 환경을 활성화합니다.
mysite\env\Scripts\activate.bat
# mysite\env\Scripts\activate.bat가 작동하지 않으면 다음을 실행하십시오.
mysite\env\Scripts\activate
GNU/Linux 또는 MacOS에서 (bash):
다음을 사용하여 가상 환경을 만듭니다.
python -m venv mysite/env
다음을 사용하여 가상 환경을 활성화합니다.
source mysite/env/bin/activate
활성화되면 명령줄에 (env) 가 표시되어 현재 이 가상 환경 내에서 작업하고 있음을 나타냅니다.
다른 셸의 경우 venv 설명서를 참조하십시오.
참고
git과 같은 버전 제어를 사용하는 경우 mysite 는 프로젝트의 디렉터리입니다.
모든 버전 제어에서 env 디렉터리를 제외해야 합니다.
Wagtail 설치¶
Wagtail 및 해당 종속성을 설치하려면 Python과 함께 패키지된 pip를 사용하십시오.
pip install wagtail
사이트 생성¶
Wagtail은 django-admin startproject 와 유사한 start 명령을 제공합니다. 프로젝트에서 wagtail start mysite 를 실행하면 필요한 프로젝트 설정, 비어 있는 HomePage 모델과 기본 템플릿이 있는 “home” 앱, 샘플 “search” 앱을 포함하여 몇 가지 Wagtail 관련 추가 기능이 있는 새 mysite 폴더가 생성됩니다.
mysite 폴더가 이미 venv 에 의해 생성되었으므로 추가 인수를 사용하여 wagtail start 를 실행하여 대상 디렉터리를 지정합니다.
wagtail start mysite mysite
생성된 프로젝트 구조는 다음과 같습니다.
mysite/
├── .dockerignore
├── Dockerfile
├── home/
├── manage.py*
├── mysite/
├── requirements.txt
└── search/
프로젝트 종속성 설치¶
cd mysite
pip install -r requirements.txt
이렇게 하면 방금 만든 프로젝트에 대한 관련 버전의 Wagtail, Django 및 기타 종속성이 있는지 확인합니다.
requirements.txt 파일에는 프로젝트를 실행하는 데 필요한 모든 종속성이 포함되어 있습니다.
데이터베이스 생성¶
기본적으로 데이터베이스는 SQLite입니다. 데이터베이스 테이블을 프로젝트의 모델과 일치시키려면 다음 명령을 실행하십시오.
python manage.py migrate
이 명령은 데이터베이스의 테이블이 프로젝트의 모델과 일치하는지 확인합니다. 모델을 변경할 때마다 python manage.py migrate 명령을 실행하여 데이터베이스를 업데이트해야 합니다. 예를 들어 모델에 필드를 추가하면 명령을 실행해야 합니다.
관리자 사용자 생성¶
python manage.py createsuperuser
이렇게 하면 전체 권한이 있는 새 관리자 사용자 계정을 만들라는 메시지가 표시됩니다. 보안상의 이유로 입력하는 동안 암호 텍스트가 표시되지 않는다는 점에 유의하는 것이 중요합니다.
서버 시작¶
python manage.py runserver
서버가 시작된 후 http://127.0.0.1:8000으로 이동하여 Wagtail의 시작 페이지를 확인하십시오.

참고
이 튜토리얼에서는 개발 서버의 URL로 http://127.0.0.1:8000 을 사용하지만 설정에 따라 다른 IP 주소나 포트일 수 있습니다. 로컬 사이트의 올바른 URL을 확인하려면 manage.py runserver 의 콘솔 출력을 읽으십시오.
이제 createsuperuser 로 관리자 사용자를 만들 때 입력한 사용자 이름과 암호로 http://127.0.0.1:8000/admin에 로그인하여 관리자 인터페이스에 액세스할 수 있습니다.

HomePage 모델 확장¶
기본적으로 “home” 앱은 models.py 에 비어 있는 HomePage 모델을 정의하고 홈페이지를 만들고 Wagtail을 사용하도록 구성하는 마이그레이션을 정의합니다.
home/models.py 를 다음과 같이 편집하여 모델에 body 필드를 추가합니다.
from django.db import models
from wagtail.models import Page
from wagtail.fields import RichTextField
class HomePage(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + ["body"]
body 는 특수 Wagtail 필드인 RichTextField 입니다. blank=True 일 때
필드가 필수가 아니며 비워 둘 수 있음을 의미합니다. Django 핵심 필드 중 하나를 사용할 수 있습니다. content_panels 는 편집 인터페이스의 기능과 레이아웃을 정의합니다. content_panels 에 필드를 추가하면 Wagtail 관리자 인터페이스에서 편집할 수 있습니다. 이에 대한 자세한 내용은 페이지 모델에서 읽을 수 있습니다.
실행:
# 마이그레이션 파일을 만듭니다.
python manage.py makemigrations
# 마이그레이션을 실행하고 모델 변경 사항으로 데이터베이스를 업데이트합니다.
python manage.py migrate
모델 정의를 변경할 때마다 앞의 명령을 실행해야 합니다. 터미널의 예상 출력은 다음과 같습니다.
Migrations for 'home':
home/migrations/0003_homepage_body.py
- Add field body to homepage
Operations to perform:
Apply all migrations: admin, auth, contenttypes, home, sessions, taggit, wagtailadmin, wagtailcore, wagtaildocs, wagtailembeds, wagtailforms, wagtailimages, wagtailredirects, wagtailsearch, wagtailusers
Running migrations:
Applying home.0003_homepage_body... OK
이제 Wagtail 관리자 인터페이스 내에서 홈페이지를 편집할 수 있습니다. 사이드바에서 페이지로 이동하여 홈 옆에 있는 편집을 클릭하여 새 본문 필드를 확인합니다.

본문 필드에 “새 사이트에 오신 것을 환영합니다!”라는 텍스트를 입력하고 초안 저장 대신 페이지 편집기 하단의 게시를 선택하여 페이지를 게시합니다.
모델에 대한 변경 사항을 반영하도록 페이지 템플릿을 업데이트해야 합니다.
Wagtail은 일반 Django 템플릿을 사용하여 각 페이지
유형을 렌더링합니다. 기본적으로 앱과 모델 이름으로 구성된 템플릿 파일 이름을 찾고
대문자를 밑줄로 구분합니다. 예를 들어 “home” 앱 내의 HomePage 는
home/home_page.html 이 됩니다. 이 템플릿 파일은
Django의 템플릿 규칙이 인식하는 모든 위치에 있을 수 있습니다. 일반적으로 앱 내의 templates 폴더에 배치할 수 있습니다.
home/templates/home/home_page.html 을 편집하여 다음 내용을 포함하도록 합니다.
{% extends "base.html" %}
<!-- 다음을 추가하여 wagtailcore_tags 로드: -->
{% load wagtailcore_tags %}
{% block body_class %}template-homepage{% endblock %}
<!-- 아래 모든 것을 다음으로 바꿉니다. -->
{% block content %}
{{ page.body|richtext }}
{% endblock %}
base.html 은 부모 템플릿을 참조합니다. 항상 템플릿에서 사용하는 첫 번째 템플릿 태그여야 합니다. 이 템플릿에서 확장하면 코드를 다시 작성하지 않아도 되고 앱 전체의 페이지가 비슷한 프레임을 공유할 수 있습니다. 자식 템플릿에서 블록 태그를 사용하면 부모 템플릿 내의 특정 콘텐츠를 재정의할 수 있습니다.
또한 템플릿 상단에 wagtailcore_tags 를 로드하고 Django에서 제공하는 태그에 추가 태그를 제공해야 합니다.

Wagtail 템플릿 태그¶
Django의 템플릿 태그 및 필터 외에도
Wagtail은 자체 템플릿 태그 및 필터를 여러 개 제공하며,
템플릿 파일 상단에 {% load wagtailcore_tags %} 를 포함하여 로드할 수 있습니다.
이 튜토리얼에서는 richtext 필터를 사용하여 RichTextField 의 내용을
이스케이프하고 인쇄합니다.
{% load wagtailcore_tags %}
{{ page.body|richtext }}
생성:
<p>새 사이트에 오신 것을 환영합니다!</p>
참고: Wagtail의 태그를 사용하는 각
템플릿에 {% load wagtailcore_tags %} 를 포함해야 합니다. 태그가 로드되지 않으면 Django에서 TemplateSyntaxError 가 발생합니다.
기본 블로그¶
이제 블로그를 만들 준비가 되었습니다. 다음 명령줄을 사용하여 Wagtail 프로젝트에 새 앱을 만듭니다.
python manage.py startapp blog
mysite/settings/base.py 의 INSTALLED_APPS 에 새 blog 앱을 추가합니다.
INSTALLED_APPS = [
"blog", # <- 새 블로그 앱.
"home",
"search",
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
#... 다른 패키지
]
참고
mysite/settings 디렉터리의 base.py 파일의 INSTALLED_APPS 섹션 내에 모든 앱을 등록해야 합니다. 이 파일을 보고 start 명령이 프로젝트의 앱을 나열하는 방법을 확인하십시오.
블로그 색인 및 게시물¶
블로그에 대한 간단한 색인 페이지를 만드는 것으로 시작합니다. blog/models.py 를 편집하여 다음을 포함하도록 합니다.
from django.db import models
# 다음을 추가합니다.
from wagtail.models import Page
from wagtail.fields import RichTextField
class BlogIndexPage(Page):
intro = RichTextField(blank=True)
content_panels = Page.content_panels + ["intro"]
앱에 새 모델을 추가했으므로 데이터베이스 마이그레이션을 만들고 실행해야 합니다.
python manage.py makemigrations
python manage.py migrate
또한 모델 이름이 BlogIndexPage 이므로 재정의하지 않는 한 기본 템플릿 이름은
blog_index_page.html 입니다. Django는 블로그 앱 폴더의 템플릿 디렉터리 내에서 페이지 모델의 이름과 일치하는 이름의 템플릿을 찾습니다. 원하는 경우 이 기본 동작을 재정의할 수 있습니다. BlogIndexPage 모델에 대한 템플릿을 만들려면 blog/templates/blog/blog_index_page.html 위치에 파일을 만듭니다.
참고
blog 앱 폴더 내에 templates/blog 폴더를 만들어야 합니다.
blog_index_page.html 파일에 다음 내용을 입력합니다.
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-blogindexpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<div class="intro">{{ page.intro|richtext }}</div>
{% for post in page.get_children %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
{{ post.specific.intro }}
{{ post.specific.body|richtext }}
{% endfor %}
{% endblock %}
get_children 을 사용하는 것 외에 앞의 blog_index_page.html 템플릿은 home_page.html 템플릿에 대한 이전 작업과 유사합니다. 튜토리얼 뒷부분에서 get_children 의 사용에 대해 배웁니다.
Django 배경이 있는 경우 pageurl 태그가 Django의 url 태그와 유사하지만 추가 인수로 Wagtail 페이지 개체를 사용한다는 것을 알 수 있습니다.
이제 이것이 완료되었으므로 Wagtail 관리자 인터페이스에서 페이지를 만드는 방법은 다음과 같습니다.
http://127.0.0.1:8000/admin으로 이동하여 관리자 사용자 정보로 로그인합니다.
Wagtail 관리자 인터페이스에서 페이지로 이동한 다음 홈을 클릭합니다.
화면 상단의
+아이콘(자식 페이지 추가)을 클릭하여 홈 페이지에 자식 페이지를 추가합니다.페이지 유형 목록에서 블로그 색인 페이지를 선택합니다.
페이지 제목으로 “블로그”를 사용하고 홍보 탭에 슬러그 “blog”가 있는지 확인하고 게시합니다.
이제 사이트에서 URL http://127.0.0.1:8000/blog에 액세스할 수 있습니다. 홍보 탭의 슬러그가 페이지 URL을 정의하는 방법을 확인하십시오.
이제 블로그 게시물에 대한 모델과 템플릿을 만듭니다. blog/models.py 를 편집하여 다음을 포함하도록 합니다.
from django.db import models
from wagtail.models import Page
from wagtail.fields import RichTextField
# BlogIndexPage 모델의 정의를 유지하고 BlogPage 모델을 추가합니다.
class BlogPage(Page):
date = models.DateField("게시 날짜")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
content_panels = Page.content_panels + ["date", "intro", "body"]
models.py 파일의 새로운 변경 사항으로 인해 데이터베이스를 다시 마이그레이션해야 합니다.
python manage.py makemigrations
python manage.py migrate
blog/templates/blog/blog_page.html 위치에 새 템플릿 파일을 만듭니다. 이제 blog_page.html 파일에 다음 내용을 추가합니다.
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-blogpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
<div class="intro">{{ page.intro }}</div>
{{ page.body|richtext }}
<p><a href="{{ page.get_parent.url }}">블로그로 돌아가기</a></p>
{% endblock %}
이 게시물이 속한 블로그의 URL을 얻기 위해 Wagtail의 내장 get_parent() 메서드를 사용하는 것에 유의하십시오.
이제 관리자 인터페이스로 이동하여 다음 단계에 따라 BlogIndexPage 의 자식으로 몇 개의 블로그 게시물을 만듭니다.
Wagtail 사이드바에서 페이지를 클릭한 다음 홈을 클릭합니다.
블로그 위로 마우스를 가져가 자식 페이지 추가를 클릭합니다.

페이지 유형, 블로그 페이지를 선택합니다.

원하는 내용으로 필드를 채웁니다.

서식 있는 텍스트 본문 필드에서 링크를 추가하려면 링크를 첨부할 텍스트를 강조 표시합니다. 이제 아이콘으로 표시된 여러 작업이 있는 팝업 모달을 볼 수 있습니다. 적절한 아이콘을 클릭하여 링크를 추가합니다. 필드 왼쪽에 나타나는 + 아이콘을 클릭하여 팝업 모달에 표시된 것과 유사한 작업을 얻을 수도 있습니다.
이미지를 추가하려면 Enter 키를 눌러 필드의 다음 줄로 이동합니다. 그런 다음 + 아이콘을 클릭하고 작업 목록에서 이미지를 선택하여 이미지를 추가합니다.
참고
Wagtail은 다양한 부모 콘텐츠 유형 아래에서 만들 수 있는 콘텐츠 종류를 완벽하게 제어할 수 있습니다. 기본적으로 모든 페이지 유형은 다른 모든 페이지 유형의 자식이 될 수 있습니다.
편집이 끝나면 각 블로그 게시물을 게시합니다.
축하합니다! 이제 작동하는 블로그의 시작을 갖게 되었습니다. 브라우저에서 http://127.0.0.1:8000/blog으로 이동하면 앞의 단계에 따라 만든 모든 게시물을 볼 수 있습니다.

제목은 게시물 페이지에 연결되어야 하며 블로그 홈페이지로 돌아가는 링크는 각 게시물 페이지의 바닥글에 나타나야 합니다.
부모와 자식¶
Wagtail의 많은 작업은 노드와 리프로 구성된 _계층적 트리 구조_의 개념을 중심으로 이루어집니다. 이에 대한 자세한 내용은 이론에서 읽을 수 있습니다. 이 경우 BlogIndexPage 는 노드 역할을 하고 개별 BlogPage 인스턴스는 _리프_를 나타냅니다.
blog_index_page.html 의 내용을 다시 살펴보겠습니다.
{% for post in page.get_children %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
{{ post.specific.intro }}
{{ post.specific.body|richtext }}
{% endfor %}
Wagtail의 모든 “페이지”는 계층 구조의 위치에서 부모 또는 자식을 호출할 수 있습니다. 하지만 왜 post.intro 가 아닌 post.specific.intro 를 지정해야 할까요? 이는 모델 class BlogPage(Page) 를 정의하는 방식과 관련이 있습니다. get_children() 메서드는 Page 기본 클래스의 인스턴스 목록을 가져옵니다. 기본 클래스에서 상속하는 인스턴스의 속성을 참조하려면 Wagtail은 실제 BlogPage 레코드를 검색하는 specific 메서드를 제공합니다. “title” 필드는 기본 Page 모델에 있지만 “intro”는 BlogPage 모델에만 있습니다. 따라서 액세스하려면 .specific 이 필요합니다.
Django with 태그를 사용하여 템플릿 코드를 단순화할 수 있습니다. 이제 blog_index_page.html 을 수정합니다.
{% for post in page.get_children %}
{% with post=post.specific %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
<p>{{ post.intro }}</p>
{{ post.body|richtext }}
{% endwith %}
{% endfor %}
더 많은 사용자 정의 Wagtail 코드를 작성하기 시작하면 계층 구조를 탐색하는 데 도움이 되는 전체 QuerySet 수정자 세트를 찾을 수 있습니다.
# 페이지 개체 'somepage'가 주어지면:
MyModel.objects.descendant_of(somepage)
child_of(page) / not_child_of(somepage)
ancestor_of(somepage) / not_ancestor_of(somepage)
parent_of(somepage) / not_parent_of(somepage)
sibling_of(somepage) / not_sibling_of(somepage)
# ... 그리고 ...
somepage.get_children()
somepage.get_ancestors()
somepage.get_descendants()
somepage.get_siblings()
자세한 내용은 페이지 QuerySet 참조를 참조하십시오.
컨텍스트 재정의¶
예리한 눈으로 블로그 색인 페이지에 문제가 있음을 눈치챘을 수 있습니다.
게시물은 시간순으로 정렬됩니다. 일반적으로 블로그는 역 시간순으로 콘텐츠를 표시합니다.
게시물 초안이 표시됩니다. 게시된 콘텐츠만 표시되도록 해야 합니다.
이를 달성하려면 템플릿에서 색인
페이지의 자식을 가져오는 것 이상을 해야 합니다. 대신 모델 정의에서
QuerySet을 수정해야 합니다. Wagtail은 재정의 가능한 get_context() 메서드를 통해 이를 가능하게 합니다.
BlogIndexPage 모델을 수정합니다.
class BlogIndexPage(Page):
intro = RichTextField(blank=True)
# get_context 메서드 추가:
def get_context(self, request):
# 게시된 게시물만 포함하도록 컨텍스트 업데이트, 역순으로 정렬
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
context['blogpages'] = blogpages
return context
# ...
변경 사항에 대한 간략한 설명은 다음과 같습니다.
원래 컨텍스트를 검색했습니다.
사용자 정의 QuerySet 수정자를 만들었습니다.
검색된 컨텍스트에 사용자 정의 QuerySet 수정자를 추가했습니다.
수정된 컨텍스트를 뷰에 반환했습니다.
blog_index_page.html 템플릿도 약간 수정해야 합니다. 변경:
{% for post in page.get_children %} 에서 {% for post in blogpages %} 로
이제 게시물 중 하나를 게시 취소합니다. 게시 취소된 게시물은 블로그의 색인 페이지에서 사라져야 합니다. 또한 나머지 게시물은 이제 가장 최근에 게시된 게시물이 먼저 오도록 정렬되어야 합니다.
이미지¶
다음에 추가해야 할 기능은 블로그 게시물에 이미지 갤러리를 첨부하는 기능입니다. 서식 있는 텍스트 body 필드에 이미지를 단순히 삽입하는 것도 가능하지만 갤러리 이미지를 데이터베이스 내의 새로운 전용 개체 유형으로 설정하면 몇 가지 이점이 있습니다. 이렇게 하면 필드 내에서 특정 방식으로 레이아웃을 지정하는 대신 템플릿에서 이미지의 레이아웃과 스타일을 완전히 제어할 수 있습니다. 또한 블로그 텍스트와 독립적으로 다른 곳에서 이미지를 사용할 수 있습니다. 예를 들어 블로그의 색인 페이지에 썸네일을 표시하는 것입니다.
이제 BlogPage 모델을 수정하고 blog/models.py 에 새 BlogPageGalleryImage 모델을 추가합니다.
# ParentalKey, Orderable에 대한 새 가져오기 추가
from modelcluster.fields import ParentalKey
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
# ... BlogIndexPage의 정의를 유지하고 BlogPage의 content_panels을 업데이트하고 새 BlogPageGalleryImage 모델을 추가합니다.
class BlogPage(Page):
date = models.DateField("게시 날짜")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
"date", "intro", "body",
# 이것을 추가합니다.
"gallery_images",
]
class BlogPageGalleryImage(Orderable):
page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='gallery_images')
image = models.ForeignKey(
'wagtailimages.Image', on_delete=models.CASCADE, related_name='+'
)
caption = models.CharField(blank=True, max_length=250)
panels = ["image", "caption"]
python manage.py makemigrations 및 python manage.py migrate 를 실행합니다.
여기에는 몇 가지 새로운 개념이 있습니다.
Orderable에서 상속하면 모델에sort_order필드가 추가되어 갤러리의 이미지 순서를 추적합니다.BlogPage에 대한ParentalKey는 갤러리 이미지를 특정 페이지에 첨부하는 것입니다.ParentalKey는ForeignKey와 유사하게 작동하지만BlogPageGalleryImage를BlogPage모델의 “자식”으로 정의하여 검토를 위해 제출하고 개정 내역을 추적하는 것과 같은 작업에서 페이지의 기본 부분으로 처리되도록 합니다.image는 실제 이미지를 저장하는 Wagtail의 내장Image모델에 대한ForeignKey입니다. 이것은 페이지 편집기에서 기존 이미지를 선택하거나 새 이미지를 업로드하기 위한 팝업 인터페이스로 나타납니다. 이렇게 하면 이미지가 여러 갤러리에 존재할 수 있습니다. 이것은 페이지와 이미지 간의 다대다 관계를 만듭니다.외래 키에
on_delete=models.CASCADE를 지정하면 시스템에서 이미지를 삭제하면 갤러리 항목도 삭제됩니다. 다른 상황에서는 갤러리 항목을 그대로 두는 것이 적절할 수 있습니다. 예를 들어 “우리 직원” 페이지에 헤드샷이 있는 사람 목록이 포함되어 있고 해당 사진 중 하나를 삭제하지만 사진 없이 페이지에 사람을 그대로 두는 것을 선호하는 경우입니다. 이 경우 외래 키를blank=True, null=True, on_delete=models.SET_NULL로 설정해야 합니다.마지막으로
BlogPage.content_panels에InlinePanel을 추가하면BlogPage의 편집 인터페이스에서 갤러리 이미지를 사용할 수 있습니다.
blog/models.py 를 편집한 후 사이드바에 이미지가 표시되고 블로그 게시물의 편집 화면에 이미지를 업로드하고 캡션을 제공하는 옵션이 있는 갤러리 이미지 필드가 표시되어야 합니다.
블로그 페이지 템플릿 blog_page.html 을 편집하여 이미지 섹션을 포함하도록 합니다.
<!-- wagtailimages_tags 로드: -->
{% load wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-blogpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
<div class="intro">{{ page.intro }}</div>
{{ page.body|richtext }}
<!-- 이것을 추가합니다. -->
{% for item in page.gallery_images.all %}
<div style="float: inline-start; margin: 10px">
{% image item.image fill-320x240 %}
<p>{{ item.caption }}</p>
</div>
{% endfor %}
<p><a href="{{ page.get_parent.url }}">블로그로 돌아가기</a></p>
{% endblock %}
블로그 페이지 템플릿을 편집한 후 이미지를 표시하려면 Wagtail 관리자에서 블로그 페이지를 편집할 때 일부 이미지를 업로드해야 합니다.
여기서는 템플릿 상단에서 가져온 wagtailimages_tags 라이브러리에 있는 {% image %} 태그를 사용하여 <img> 요소를 삽입하고 fill-320x240 매개변수를 사용하여 이미지를 320x240 직사각형으로 채우도록 크기를 조정하고 자릅니다. 템플릿에서 이미지 사용에 대한 자세한 내용은 문서에서 읽을 수 있습니다.

갤러리 이미지는 자체적으로 데이터베이스 개체이므로 이제 블로그 게시물 본문과 독립적으로 쿼리하고 재사용할 수 있습니다. 이제 BlogPage 모델에 main_image 메서드를 정의합니다. 이 메서드는 첫 번째 갤러리 항목의 이미지를 반환하거나 갤러리 항목이 없으면 None 을 반환합니다.
class BlogPage(Page):
date = models.DateField("게시 날짜")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
# main_image 메서드 추가:
def main_image(self):
gallery_item = self.gallery_images.first()
if gallery_item:
return gallery_item.image
else:
return None
content_panels = Page.content_panels + ["date", "intro", "body", "gallery_images"]
이 메서드는 이제 템플릿에서 사용할 수 있습니다. blog_index_page.html 을 업데이트하여 wagtailimages_tags 라이브러리를 로드하고 각 게시물 옆에 기본 이미지를 썸네일로 포함하도록 합니다.
<!-- wagtailimages_tags 로드: -->
{% load wagtailcore_tags wagtailimages_tags %}
<!-- 이것을 수정합니다. -->
{% for post in blogpages %}
{% with post=post.specific %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
<!-- 이것을 추가합니다. -->
{% with post.main_image as main_image %}
{% if main_image %}{% image main_image fill-160x100 %}{% endif %}
{% endwith %}
<p>{{ post.intro }}</p>
{{ post.body|richtext }}
{% endwith %}
{% endfor %}
작성자¶
블로그 게시물에는 작성자가 있어야 하며 이는 블로그의 필수 기능입니다. 이를 수행하는 방법은 사이트 소유자가 관리자 인터페이스의 별도 영역을 통해 관리하는 고정 목록을 갖는 것입니다.
먼저 Author 모델을 정의합니다. 이 모델은 자체적으로 페이지가 아닙니다. Page 에서 상속하는 대신 표준 Django models.Model 로 정의해야 합니다. Wagtail은 페이지 트리의 일부로 존재하지 않는 재사용 가능한 콘텐츠 조각에 대해 스니펫 개념을 도입합니다. 관리자 인터페이스를 통해 스니펫을 관리할 수 있습니다. @register_snippet 데코레이터를 추가하여 모델을 스니펫으로 등록할 수 있습니다. 또한 페이지에서 지금까지 사용한 모든 필드 유형을 스니펫에서도 사용할 수 있습니다.
작성자를 만들고 각 작성자에게 작성자 이미지와 이름을 부여하려면 blog/models.py 에 다음을 추가합니다.
# 파일 상단에 이것을 추가합니다.
from wagtail.snippets.models import register_snippet
# ... BlogIndexPage, BlogPage, BlogPageGalleryImage 모델을 유지한 다음 Author 모델을 추가합니다.
@register_snippet
class Author(models.Model):
name = models.CharField(max_length=255)
author_image = models.ForeignKey(
'wagtailimages.Image', null=True, blank=True,
on_delete=models.SET_NULL, related_name='+'
)
panels = ["name", "author_image"]
def __str__(self):
return self.name
class Meta:
verbose_name_plural = 'Authors'
참고
여기서는 content_panels 가 아닌 panels 를 사용하고 있습니다. 스니펫은 일반적으로 슬러그나 게시 날짜와 같은 필드가 필요하지 않으므로 편집 인터페이스가 별도의 ‘콘텐츠’ / ‘홍보’ / ‘설정’ 탭으로 분할되지 않습니다. 따라서 ‘콘텐츠 패널’과 ‘홍보 패널’을 구별할 필요가 없습니다.
python manage.py makemigrations 및 python manage.py migrate 를 실행하여 이 변경 사항을 마이그레이션합니다. 이제 Wagtail 관리자 인터페이스에 나타나는 스니펫 영역을 통해 몇 명의 작성자를 만듭니다.
이제 BlogPage 모델에 작성자를 다대다 필드로 추가할 수 있습니다. 이에 사용할 필드 유형은 ParentalManyToManyField 입니다. 이 필드는 선택한 개체가 개정 내역에서 페이지 레코드와 올바르게 연결되도록 하는 표준 Django ManyToManyField 의 변형입니다. ParentalKey 가 일대다 관계에 대해 ForeignKey 를 대체하는 방식과 유사하게 작동합니다. BlogPage 에 작성자를 추가하려면 블로그 앱 폴더의 models.py 를 수정합니다.
# ParentalManyToManyField 및 MultiFieldPanel에 대한 새 가져오기 추가
from django.db import models
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import MultiFieldPanel
from wagtail.snippets.models import register_snippet
class BlogPage(Page):
date = models.DateField("게시 날짜")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
# 이것을 추가합니다.
authors = ParentalManyToManyField('blog.Author', blank=True)
# ... main_image 메서드를 유지합니다. content_panels을 수정합니다.
content_panels = Page.content_panels + [
MultiFieldPanel(["date", "authors"], heading="블로그 정보"),
"intro", "body", "gallery_images"
]
여기서는 가독성을 위해 date 및 authors 필드를 함께 그룹화하기 위해 content_panels 에서 MultiFieldPanel 을 사용했습니다. 이렇게 하면 목록 또는 튜플 내의 여러 필드를 단일 heading 문자열로 캡슐화하는 단일 패널 개체를 만듭니다. 이 기능은 관리자 인터페이스에서 관련 필드를 구성하는 데 특히 유용하여 콘텐츠 편집자에게 UI를 더 직관적으로 만듭니다.
python manage.py makemigrations 및 python manage.py migrate 를 실행하여 데이터베이스를 마이그레이션한 다음 관리자 인터페이스로 이동합니다. 작성자 목록이 다중 선택 상자로 표시됩니다. 이것은 다중 선택 필드의 기본 표현이지만 사용자는 종종 체크박스 세트가 더 친숙하고 작업하기 쉽다고 생각합니다.

content_panels의 "authors" 정의를 FieldPanel 개체로 바꾸면 됩니다. FieldPanel("authors") 는 "authors" 를 작성하는 것과 동일하지만 widget 과 같은 추가 선택적 인수를 전달할 수 있습니다.
# forms 및 FieldPanel에 대한 새 가져오기 추가
from django import forms
from django.db import models
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.snippets.models import register_snippet
class BlogPage(Page):
date = models.DateField("게시 날짜")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
authors = ParentalManyToManyField('blog.Author', blank=True)
content_panels = Page.content_panels + [
MultiFieldPanel([
"date",
# 이것을 변경합니다.
FieldPanel("authors", widget=forms.CheckboxSelectMultiple),
], heading="블로그 정보"),
"intro", "body", "gallery_images"
]
앞의 모델 수정에서 FieldPanel 정의의 widget 키워드 인수를 사용하여 기본 목록 대신 더 사용자 친화적인 체크박스 기반 위젯을 지정했습니다. 이제 관리자 인터페이스로 이동하면 작성자 목록이 체크리스트로 표시되어야 합니다.

작성자를 표시하도록 blog_page.html 템플릿을 업데이트합니다.
{% block content %}
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
<!-- 이것을 추가합니다. -->
{% with authors=page.authors.all %}
{% if authors %}
<h3>게시자:</h3>
<ul>
{% for author in authors %}
<li style="display: inline">
{% image author.author_image fill-40x60 style="vertical-align: middle" %}
{{ author.name }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div class="intro">{{ page.intro }}</div>
{{ page.body|richtext }}
{% for item in page.gallery_images.all %}
<div style="float: inline-start; margin: 10px">
{% image item.image fill-320x240 %}
<p>{{ item.caption }}</p>
</div>
{% endfor %}
<p><a href="{{ page.get_parent.url }}">블로그로 돌아가기</a></p>
{% endblock %}
블로그 게시물에 일부 작성자를 추가하고 게시합니다. 블로그 색인 페이지에서 블로그 게시물을 클릭하면 이제 이 이미지와 유사한 페이지가 표시됩니다.

태그 게시물¶
편집자가 게시물에 “태그”를 지정하여 독자가 예를 들어
자전거 관련 콘텐츠를 모두 함께 볼 수 있도록 하고 싶다고 가정해 보겠습니다. 이를 위해
Wagtail과 함께 제공되는 태그 지정 시스템을 호출하고 BlogPage
모델 및 콘텐츠 패널에 연결하고 블로그 게시물 템플릿에 연결된 태그를 렌더링해야 합니다.
물론 작동하는 태그별 URL 뷰도 필요합니다.
먼저 models.py 를 다시 한 번 변경합니다.
from django import forms
from django.db import models
# ClusterTaggableManager, TaggedItemBase에 대한 새 가져오기 추가
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
# ... BlogIndexPage 모델의 정의를 유지하고 새 BlogPageTag 모델을 추가합니다.
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey(
'BlogPage',
related_name='tagged_items',
on_delete=models.CASCADE
)
# BlogPage 모델 수정:
class BlogPage(Page):
date = models.DateField("게시 날짜")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
authors = ParentalManyToManyField('blog.Author', blank=True)
# 이것을 추가합니다.
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
# ... main_image 메서드를 유지합니다. 그런 다음 content_panels을 수정합니다.
content_panels = Page.content_panels + [
MultiFieldPanel([
"date",
FieldPanel("authors", widget=forms.CheckboxSelectMultiple),
# 이것을 추가합니다.
"tags",
], heading="블로그 정보"),
"intro", "body", "gallery_images"
]
python manage.py makemigrations 및 python manage.py migrate 를 실행합니다.
변경 사항은 다음과 같이 요약할 수 있습니다.
새로운
modelcluster및taggit가져오기새로운
BlogPageTag모델 및BlogPage의tags필드 추가.
BlogPage 인스턴스 중 하나를 편집하면 이제 게시물에 태그를 지정할 수 있습니다.

BlogPage 에 태그를 렌더링하려면 blog_page.html 에 다음을 추가합니다.
<p><a href="{{ page.get_parent.url }}">블로그로 돌아가기</a></p>
<!-- 이것을 추가합니다. -->
{% with tags=page.tags.all %}
{% if tags %}
<div class="tags">
<h3>태그</h3>
{% for tag in tags %}
<a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
{% endfor %}
</div>
{% endif %}
{% endwith %}
여기서는 이전에 사용했던 pageurl 대신 내장된 slugurl
태그를 사용하여 페이지에 연결하고 있습니다. 차이점은 slugurl 은 Page 슬러그(홍보 탭에서)를 인수로 사용한다는 것입니다. pageurl 은 명확하고 추가 데이터베이스 조회를 피하기 때문에 더 일반적으로 사용됩니다. 그러나 이 루프의 경우 Page 개체를 쉽게 사용할 수 없으므로 덜 선호되는 slugurl 태그로 대체합니다.
지금까지 만든 수정 사항으로 태그가 있는 블로그 게시물을 방문하면 게시물과 연결된 각 태그에 대해 하나씩 연결된 버튼 시리즈가 하단에 표시됩니다. 그러나 버튼을 클릭하면 아직 “태그” 뷰를 정의하지 않았기 때문에 404 오류 페이지가 표시됩니다.
blog/models.py 로 돌아가서 새 BlogTagIndexPage 모델을 추가합니다.
class BlogTagIndexPage(Page):
def get_context(self, request):
# 태그로 필터링
tag = request.GET.get('tag')
blogpages = BlogPage.objects.filter(tags__name=tag)
# 템플릿 컨텍스트 업데이트
context = super().get_context(request)
context['blogpages'] = blogpages
return context
이 페이지 기반 모델은 자체 필드를 정의하지 않습니다.
필드가 없어도 Page 를 서브클래싱하면
Wagtail 생태계의 일부가 되므로
관리자에서 제목과 URL을 지정할 수 있습니다. 또한 get_context() 메서드를 재정의하여
컨텍스트 사전에 QuerySet을 추가하여 템플릿에서 사용할 수 있도록 할 수 있습니다.
python manage.py makemigrations 를 실행한 다음 python manage.py migrate 를 실행하여 마이그레이션합니다. 새 변경 사항을 마이그레이션한 후 관리자 인터페이스에서 새 BlogTagIndexPage 를 만듭니다. BlogTagIndexPage 를 만들려면 BlogIndexPage 를 만들 때와 동일한 프로세스를 따르고 홍보 탭에서 슬러그 “tags”를 지정합니다. 즉, BlogTagIndexPage 는 홈 페이지의 자식이며 관리자 인터페이스에서 Blog 와 병렬입니다.
/tags 에 액세스하면 Django가 이미 알고 있을 가능성이 있는 것을 알려줍니다.
템플릿 blog/templates/blog/blog_tag_index_page.html 을 만들고 다음 내용을 추가해야 합니다.
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
{% if request.GET.tag %}
<h4>"{{ request.GET.tag }}" 태그가 지정된 페이지 표시</h4>
{% endif %}
{% for blogpage in blogpages %}
<p>
<strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
<small>수정됨: {{ blogpage.latest_revision_created_at }}</small><br />
</p>
{% empty %}
해당 태그가 있는 페이지를 찾을 수 없습니다.
{% endfor %}
{% endblock %}
앞의 blog_tag_index_page.html 템플릿에서는 Page 모델의 내장 latest_revision_created_at 필드를 호출하고 있습니다. 이것이 항상 사용 가능하다는 것을 아는 것이 편리합니다.
블로그 게시물 하단의 태그 버튼을 클릭하면 다음과 같은 페이지가 렌더링됩니다.

축하합니다!¶
이 튜토리얼을 완료했습니다 🥳. 자신에게 박수를 보내고 쿠키를 드세요!
읽어 주셔서 감사하며 Wagtail 커뮤니티에 오신 것을 환영합니다!