https://docs.wagtail.io/en/stable/topics/streamfield.html

StreamField에 대해서 소개해보려고 한다. 

Wagtail에는 이렇게 말하고 있다. 'A block-based content field' 

 

StreamField는 Custom Field라고 생각하면 된다. 고정된 구조를 따르지 않고 Custom이 가능하다. admin page에서 수정을 할때 block 형태로 여러가지 content들을 추가 가능하다.

 

쉽게 말하자면 django admin의 field set 기능이랑 비슷하다고 생각하면 된다. stream field를 사용하여 page를 생성할 때, 정돈된 형태로 작성할 수 있다.

 

streamfield를 실습하기 위해 app을 하나 만들어보자 이름은 blog로 하겠다.

python manage.py startapp blog

Setting에 Installed_app에 blog를 추가해준다.

project_folder/settings/base.py

INSTALLED_APPS = [
    'home',
    'search',
    'card',
    'member',
    'classroom',
    # 테스트입니다.
    'blog',

이제 모델을 작성해보자.

from django.db import models

from wagtail.core.models import Page
from wagtail.core.fields import StreamField
from wagtail.core import blocks
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.images.blocks import ImageChooserBlock

class BlogPage(Page):
    author = models.CharField(max_length=255)
    date = models.DateField("Post date")
    body = StreamField([
        ('heading', blocks.CharBlock(form_classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
    ])

    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('date'),
        StreamFieldPanel('body'),
    ]

모델을 작성했으니 makemigrations와 migrate를 진행하자.

 

templates 폴더를 만들고 blog_page.html를 작성해보자

templates/blog/blog_page.html

{% extends "base.html" %}

{% load wagtailcore_tags %}

<article>
    {% for block in page.body %}
        {% if block.block_type == 'heading' %}
            <h1>{{ block.value }}</h1>
        {% else %}
            <section class="block-{{ block.block_type }}">
                {% include_block block %}
            </section>
        {% endif %}
    {% endfor %}
</article>

여기서 streamfield는 body라는 변수로 정의를 해놓았기 때문에 page..body로 streamfield의 내용을 출력할 수 있다.

for 반복문 안에서 if문을 사용하여 표시하고자 하는 streamfield안에 있는 field 값을 호출 해올 수 있다. 

이는 .value를 사용하여 가능하다. 

그냥 field값을 계속 출력하고 싶으면 

{% include_block block %}로 작성하면 된다.

 

python manage.py runserver를 하게 되면 admin 페이지에서 streamfield가 생성 됨을 볼 수 있다. 

간단하게 기본 사용법을 알아보았는데 사실 streamfield custom하는 방법은 엄청 많다. 

StructBlock

struckblock은 streamfield 안에 한번 더 streamfield를 정의 해준 느낌이다. 아래 코드에서는 이름에 관련 된 '자식' 블록을 생성하여 작성하였다. 

 body = StreamField([
     ('person', blocks.StructBlock([
         ('first_name', blocks.CharBlock()),
         ('surname', blocks.CharBlock()),
         ('photo', ImageChooserBlock(required=False)),
         ('biography', blocks.RichTextBlock()),
     ])),
     ('heading', blocks.CharBlock(form_classname="full title")),
     ('paragraph', blocks.RichTextBlock()),
     ('image', ImageChooserBlock()),
 ])

template에 작성할 때는 아래와 같이 작성한다. 

 

<article>
    {% for block in page.body %}
        {% if block.block_type == 'person' %}
            <div class="person">
                {% image block.value.photo width-400 %}
                <h2>{{ block.value.first_name }} {{ block.value.surname }}</h2>
                {{ block.value.biography }}
            </div>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

 

Subclassing StructBlock

Structblock안에 child block을 정의 해주면 읽기도 불편하고 재사용하기도 힘들다. 그래서 차라리 sub class를 만들어서 사용하는 방법에 대해서 알아보려고 한다. 

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

이렇게 sub class를 models.py에 정의해주자.

body = StreamField([
    ('person', PersonBlock()),
    ('heading', blocks.CharBlock(form_classname="full title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
])

그리고 클래스를 호출해오게 되면 전과 같은 기능을 하는 것을 확인할 수 있다.

 

Block Icons

 

streamfield에 icon을 추가 해줄 수 있다. page instance를 생성할 때, icon을 통해서 각각의 block 을 구별 해줄 수 있다. 

UI Guide

자세한건 UI Guide 문서를 참고하면 될 것이다.

일단 settings에 base.py에 installed_app에 styleguide를 입력하자

INSTALLED_APPS = (
    ...
    'wagtail.contrib.styleguide',
    ...
)

그리고 models.py에 icon을 추가해주자.

 body = StreamField([
     ('person', blocks.StructBlock([
         ('first_name', blocks.CharBlock()),
         ('surname', blocks.CharBlock()),
         ('photo', ImageChooserBlock(required=False)),
         ('biography', blocks.RichTextBlock()),
     ], icon='user')),
     ('heading', blocks.CharBlock(form_classname="full title")),
     ('paragraph', blocks.RichTextBlock()),
     ('image', ImageChooserBlock()),
 ])
 class PersonBlock(blocks.StructBlock):
     first_name = blocks.CharBlock()
     surname = blocks.CharBlock()
     photo = ImageChooserBlock(required=False)
     biography = blocks.RichTextBlock()

     class Meta:
         icon = 'user'

amdin을 들어가서 생성을 해보면 아이콘이 잘 적용되어 있는 것을 확인 할 수 있을 것이다.

 

ListBlock

 

listblock을 생성해보자 listblock은 반복되는 block을 뜻하는데 생성해야할 instance가 많을 때, 사용한다. 형태를 보면 바로 이해가 될 것이다. 

 body = StreamField([
     ('gallery', blocks.ListBlock(ImageChooserBlock())),
     ('heading', blocks.CharBlock(form_classname="full title")),
     ('paragraph', blocks.RichTextBlock()),
     ('image', ImageChooserBlock()),
 ])
<article>
    {% for block in page.body %}
        {% if block.block_type == 'gallery' %}
            <ul class="gallery">
                {% for img in block.value %}
                    <li>{% image img width-400 %}</li>
                {% endfor %}
            </ul>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

슬라이드나 여러 이미지를 표시해야할 때, 효과적으로 관리해줄 수 있을 것이다.

<영상을 추가하고 싶을 때는 from wagtail.embeds.blocks import EmbedBlock 을 사용한다.>

 

Limiting block counts

default에서는 streamfield에서 block을 숫자 제한 없이 사용할 수 있다. min_num과 max_num을 사용하면 Streamblock의 수를 제한할 수 있다.

4가지 방법이 있는데 코드만 집고 넘어가보자

body = StreamField([
    ('heading', blocks.CharBlock(form_classname="full title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], min_num=2, max_num=5)
class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="full title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        min_num = 2
        max_num = 5
body = StreamField([
    ('heading', blocks.CharBlock(form_classname="full title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], block_counts={
    'heading': {'min_num': 1, 'max_num': 3},
})
class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="full title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        block_counts = {
            'heading': {'min_num': 1, 'max_num': 3},
        }

 

 

추가) StreamField 사용

blocks.py

from wagtail.core import blocks
from wagtail.images.blocks import ImageChooserBlock


class TitleAndTextBlock(blocks.StructBlock):
    """Title and text and nothing else"""

    title = blocks.CharBlock(required=True, help_text="Add your title")
    text = blocks.TextBlock(required=True, help_text='Add additional text')
    
    class Meta:
        template = "streams/title_and_text_block.html"
        icon = "edit"
        label = "Title & Text"
        
        
class CardBlock(blocks.StructBlock):
    """Cards with image and text and button(s)."""
    
    title = blocks.CharBlock(required=True, help_text="Add your title")
    
    cards = blocks.ListBlock(
        blocks.StructBlock(
            [
                ("image", ImageChooserBlock(required=True)),
                ("title", blocks.CharBlock(required=True, max_length=40)),
                ("text", blocks.TextBlock(required=True, max_length=200)),
                ("button_page", blocks.PageChooserBlock(required=False)),
                ("button_url", blocks.URLBlock(required=False, help_text="If the button page above is selected, that will be used first."))
            ]
        )
    )
    
    class Meta: # noqa
        template = "streams/card_block.html"
        icon = "placeholder"
        label = "Staff Cards"   


class RichtextBlock(blocks.RichTextBlock):
    """Richtext without (limited) all the features"""
    
    class Meta: # noqa
        template = "streams/richtext_block.html"
        icon = "doc-full"
        label = "Full RichText"       
        
        
class SimpleRichtextBlock(blocks.RichTextBlock):
    """Richtext without (limited) all the features"""
    
    def __init__(self, required=True, help_text=None, editor='default', features=None, validators=(), **kwargs):
        super().__init__(**kwargs)
        self.features = [
            "bold",
            "italic",
            "link",
        ]

    class Meta: # noqa
        template = "streams/simple_richtext_block.html"
        icon = "edit"
        label = "Simple RichText"
        
        
class CTABlock(blocks.StructBlock):
    """A simple call to action section"""
    
    title = blocks.CharBlock(required=True, max_length=60)
    text = blocks.RichTextBlock(required=True, features=["bold","italic"])
    button_page = blocks.PageChooserBlock(required=False)
    button_url = blocks.URLBlock(required=False)
    button_text = blocks.CharBlock(required=True, default='Learn More', max_length=40)
    
    class Meta:
        template = "streams/cta_block.html"
        icon = "placeholder"
        label = "Call to Action"

위와 같은 StreamBlock 구조로 작성하였다.

각 Block마다 template이 정의되어 있다. 

 

 

1) RichTextBlock.html

{{ self }}

특별한 구조가 없으므로 간단하기 자기 자신을 가르키는 self로 작성한다.

 

 

2) CardBlock.html

{% load wagtailimages_tags %}

<div class="container">
    <h1 class="text-center">{{ self.title }}</h1>
    <div class="card-deck">
        {% for card in self.cards %}
            {% image card.image fill-300x200 as img %}
            <div class="card">
                <img src="{{ img.url }}" class="card-img-top" alt="{{ img.alt }}">
                <div class="card-body">
                    <h5 class="card-title">{{ card.title }}</h5>
                    <p class="card-text">{{ card.text }}</p>
                    {% if card.button_page %}
                        <a href="{{ card.button_page.url }}" class="btn btn-primary">
                            Learn More (internal)
                        </a>
                    {% elif card.button_url %}
                        <a href="{{ card.button_url }}" class="btn btn-primary">
                            Learn More (external)
                        </a>
                    {% endif %}
                </div>
            </div>
        {% endfor %}
    </div>    
</div>

title이라는 CharBlock과 cards라는 ListBlock으로 작성되어 있다. ListBlock은 StruckBlock으로 되어 있고 여러 Block들을 가지고 있다. 일반적인 Wagtail templates 작성처럼 해주면 된다. 

 

그렇다면 Streams의 Templates에서 말고 실제 보여지는 Template에서는 작성을 어떻게 할까?

 

    {% for block in page.content %}
        {% include_block block %}
    {% endfor %}

위와 같이 작성을 해주면 block에 정의된 Template으로 화면에 표현이 된다. 

'Back-End > Wagtail, Django' 카테고리의 다른 글

Wagtail demo site (breads)  (0) 2021.08.17
Wagtail demo site (blog)  (0) 2021.08.17
Wagtail demo site (base)  (0) 2021.08.15
Wagtail 이미지  (0) 2021.08.13
Wagtail tutorial (튜토리얼)  (0) 2021.08.11

 

https://docs.wagtail.io/en/stable/getting_started/tutorial.html

https://geniyong.github.io/2019/04/17/Django-Wagtail-CMS-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0.html

 

(위의 두개의 사이트를 참고하면서 작성하였다. 위에는 wagtail 공식 문서이고 아래는 번역된 블로그이다.)

 

Wagtail은 Python으로 작성된 무료 오픈 소스 컨텐츠 관리 시스템 (CMS)이다.

Django 기반으로 만들어져 있어, 기존의 django 프로젝트와 통합하기가 쉽다. 

빠르고 간단하게 자신의 Web Site를 만들어보고 싶은 사람들에게 추천한다. Wagtail 공식 문서에 기술되어 있지 않는 기능은 Django문서를 찾아서 적용하면 된다. 

 

환경설정

wagtail 튜토리얼을 진행하기 위해 새로운 anaconda3 가상환경을 만들어 주었다. 

가상환경의 이름은 wagtail이며 python version은 3.8.2로 진행하였다. 

 

운영체제 : Windows 10

파이썬 버전 : python 3.8.2

가상환경 : anaconda3

 

기본적으로 anaconda3가 설치되어 있다는 가정하에 진행하도록 하겠다. anaconda3 설치는 아래 링크를 참고하면 된다.

https://wikidocs.net/22896

(anaconda3말고 python env로 진행해도 무방하다.)

 

1. 프로젝트 생성

 

1) 아나콘다 가상환경을 만들어준다.

$ conda create -n wagtail python=3.8.2

 

 

2) 가상환경으로 바꿔준다.

$ conda activate wagtail

 

3) wagtail을 설치해준다.

$ pip install wagtail

 

4) mysite라는 이름의 wagtail 프로젝트를 생성해준다. 

$ wagtail start mysite

 

5) mysite 폴더로 이동한다.

$ cd mysite

 

6) makemigrations와 migrate를 진행해준다. 

$ python manage.py makemigrations
$ python manage.py migrate

makemigrations은 변경된 모델을 기록하는 것이고 migrate는 기록을 기반으로 세부적인 table과 field들을 생성하는 것이다. 

 

7) superuser를 생성해준다. (admin)

$ python manage.py createsuperuser

 

8) 서버를 실행해준다. 

$ python manage.py runserver

문제가 없었다면 http://127.0.0.1:8000 을 통해 wagtail에 접속되는 것을 확인할 수 있다.

http://127.0.0.1:8000/admin 는 admin 페이지 주소이다. 

 

2. HomePage 모델 확장하기 

 

home/models.py를 다음과 같이 작성한다. 

from django.db import models

from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel


class HomePage(Page):
    body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('body', classname="full"),
    ]

이는 body 필드를 추가해주기 위한 작업이며 RichTextField는 Wagtail에서 사용하는 field이다.

(RichTextField는 여러가지 에디터 툴을 제공한다. )

 

다른 django core fields를 사용해도 상관이 없다. (wagtail에서는 Django field가 사용가능하다.)

 

content_panels은 admin 페이지에서 생성 field들에 데이터를 입력할 때 사용한다.

모델을 변경했다면 바로 makemigrations와 migrate를 해줘야한다. (모델을 수정했으면 항상 실행해주자)

python manage.py makemigrations
python manage.py migrate

 

모델 변경사항을 확인해보기 위해서는 템플릿을 업데이트 해야한다. 

Wagtail에서는 Page모델의 템플릿은 대문자가 밑줄로 구분되어 기본이름이 설정이 된다.

예를들어 위에 HomePage이면 home/home_page.html 이 된다. 

(물론 templates = "url"로 템플릿을 설정 해줄 수 있다.)

 

참고) templates은 home app과 mysite폴더에 있을 것이다. 정답은 없지만 보통 mysite에서 templates와 static 파일을 추가해준다. home에 있는 static폴더와 templates 폴더를 지워주고 mysite의 templates 폴더에 home이라는 폴더를 생성하여 만들어주자. 

 

templates/home/homepage.html

{% extends "base.html" %}

{% load wagtailcore_tags %}

{% block body_class %}template-homepage{% endblock %}

{% block content %}
    {{ page.body|richtext }}
{% endblock %}

{% extends "base.html" %}을 통해 Wagtail의 기본 base.html에 정의 된 css나 js를 사용할 수 있다.

 

Wagtail은 수많은 템플릿 태그와 필터를 제공한다. {% load wagtailcore_tags %} 를 템플릿 파일의 최상단에 위치시킴으로써 불러올 수 있다.

 

RichTextField 의 내용을 escape 및 출력하기 위해 richtext filter를 사용한다. RichTextField로 만든 field들은 template에서는 항상 "|richtext"를 붙여주자. 

(사용하지 않으면 아래 처럼 RichTextField의 데이터 저장 값 그대로 출력된다. )

<div class="rich-text">
    <p>
        <b>Welcome</b> to our new site!
    </p>
</div>

 

 

주의사항 : {% load wagtailcore_tags %} 을 wagtail 의 태그를 사용하는 템플릿의 최상단에 입력해야한다. 그렇지 않으면 django 가 TemplateSyntaxError 를 통해 예외처리 할 것이다.

 

 

3. 기본 블로그 만들기

 

1) 블로그를 작성하기 위해 blog App을 만들어주자. 

python manage.py startapp blog

 

2) blog App을 settings에 추가한다. 

settings/base.py

INSTALLED_APP = [
	'blog',
]

 

이제 blog 모델을 작성해보자.

blog/models.py

from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel


class BlogIndexPage(Page):
    intro = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('intro', classname="full")
    ]

역시 makemigrations와 migrate를 진행해줘야한다. (모델을 추가했기 때문!)

Page 모델을 만들었기 때문에 templates을 추가해주자.

templates 폴더에 blog라는 폴더를 만들고 blog_index_page.html을 만들어준다. 

 

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 %}

"{% pageurl post %}"는 Django 의 url 태그와 비슷하지만 wagtail 페이지 객체만을 인자로 사용하는 pageurl 태그이다.

 

그리고 wagtail admin에서 Pages -> home -> add child page -> blog index page로 slug 값을 blog로 하고 저장할때 꼭 'publish'로 저장!!! 저장할 때 보면 save draft로 되어 있는데 publish로 바꾸어서 저장해야한다. 

 

이후 블로그 포스팅을 위해 blog/models.py에 다음 내용을 추가해주자

from django.db import models

from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.search import index


class BlogIndexPage(Page):
    intro = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('intro', classname="full")
    ]


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
    ]

역시 makemigrations와 migrate를 진행하고 blog_page.html을 추가해준다. 

blog/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 }}">Return to blog</a></p>

{% endblock %}

위 admin page에서 blog index page를 추가 한 것 처럼 blog 페이지 몇개를 추가해보자.

역시 publish로 하는 것을 주의! 이제 http://127.0.0.1:8000/blog/ 로 들어가게 되면 방금 작성한 포스트를 볼 수 있을 것이다.

 

부모와 자식

wagtail에서는 대부분 '트리' 구조를 가진다. 방금 같은 경우 BlogIndexPage는 'node'이고 BlogPage instance들은 'leaves'이다. 간단하게 설명하자면 BlogIndexPage를 부모라고 생각하고 Blogpage들을 자식이라고 생각하면 된다.

자식에서 부모의 요소를 불러오고 싶으면 get_parent 메소드를 사용하면 되고 부모에서 자식 요소를 불러오고 싶을 때는 get_children을 사용하면 된다. 

 

blog_index_page를 보게 되면

{% for post in page.get_children %}
    <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
    {{ post.specific.intro }}
    {{ post.specific.body|richtext }}
{% endfor %}

post.specific.intro로 되어 있다. 왜 post.intro가 아니라 specific을 사용할까? 이는 모델에서 지정한 방식과 관련이 있다.

 

class BlogPage(Page):

get_children() 메소드는 page라는 베이스 클래스의 인스턴스 리스트를 가져다 준다. 우리가 기본 클래스로부터 상속받은 인스턴스들의 properties를 참조하길 원할 때, wagtail은 specific 메소드를 제공한다. title 같은 경우는 기본 page 모델은 존재하지만 intro 같은 경우는 blogpage 모델에만 존재한다는 점에서 intro field에 접근하기 위해서는 specific을 이용하여 접근해야한다.

 

물론 아래와 같이 작성할 수도 있다.

{% 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 %}

 

<참고 : with 태그는 with ~ endtith 내에서 특정 값을 변수에 저장해두는 기능을 한다.>

# '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)
# ... and ...
somepage.get_children()
somepage.get_ancestors()
somepage.get_descendants()
somepage.get_siblings()

wagtail query set 참조는 아래에서 확인할 수 있다.

Query Set 레퍼런스

 

Context 오버라이딩

블로그 index에는 다음과 같이 2가지 문제점이 있다.

1. 블로그는 일반적으로 발행 시간 역순으로 내용 표시.

2. 우리는 발행된 컨텐트만을 확인하고 싶다. 

 

이를 해결하기 위해서는 단순히 가져오는 것 뿐만 아닌 무엇인가를 더 해야한다. django view에서 한 것 처럼 get_context를 이용하여 할 수 있다. 

BlogIndexPate 모델을 다음과 같이 수정하자.

 

class BlogIndexPage(Page):
 intro = RichTextField(blank=True)

 def get_context(self, request):
     # Update context to include only published posts, ordered by reverse-chron
     context = super().get_context(request)
     blogpages = self.get_children().live().order_by('-first_published_at')
     context['blogpages'] = blogpages
     return context

이 코드에서는 context에 queryset을 만들고 수정한 뒤에 context를 view에다가 돌려주었다. 

<여기서 live()란 현재 publish된 blog page만 가져 온다는 뜻이다.>

 

context를 통해 blogpages로 데이터를 넘겨줬으니 blog_index_page.html 도 수정하면 된다. 

{% for post in page.get_children %} 를 {% for post in blogpages %} 으로 수정하면 될 것이다.

context['blogpages'] = blog

 

이제 포스트 하나를 발행 취소하게 되면 blog index page에서 글이 보이지 않게 되고 최신 순으로 정렬이 되어 있을 것이다. 확인해보자.

 

이미지를 추가해보자 

 

게시물에 이미지를 추가해보는 작업을 해볼 것이다. body rich text field에 간단하게 image를 추가할 수 있지만 데이터 베이스에 객체로 저장해서 사용할 경우 템플릿에서 이미지 레이아웃과 스타일을 제어할 수 있다는 장점이 있다. 

언제 어디서나 사용할 수 있다. (예를 들어서 blog index page에 썸네일로 보여줄 수 있다.)

 

새로운 모델을 추가시켜보자. blog/models.py

 

from django.db import models

# New imports added for ParentalKey, Orderable, InlinePanel, ImageChooserPanel

from modelcluster.fields import ParentalKey

from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index


# 기존 모델은 여기에


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
        InlinePanel('gallery_images', label="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 = [
        ImageChooserPanel('image'),
        FieldPanel('caption'),
    ]

역시 모델을 추가하거나 수정했으니 makemigrations와 migrate를 진행해야한다.

 

모델에 관해 적어보자면 Orderable로 부터 상속받은 모델에서는 갤러릴의 이미지 순서를 알기 위해 sort_order 필드를 추가한다.

Parentalkey는 ForeignKey와 유사하게 실행 되지만 BlogPageGalleryImage를 BlogPage 모델의 자식으로 정의하기도 한다. 

image는 ForeignKey인데 여기에 이미지들이 저장된다. 한 이미지로 여러 갤러리를 만들 수 있다는 것은 ManytoMany에 효과적이다.

 

CASCADE는 이미지가 삭제 되었을 때, 갤러리 목록에서도 이미지를 지우게 한다. 상황에 맞게 사용하면 된다 만약에 목록에서는 지우기를 원하지 않을때는 blank=True, null=True, on_delete=models.SET_NULL 로 사용해주면 된다. 

 

InlinePanel을 BlogPage.content_panels에 추가하면 BlogPage 의 편집 인터페이스에서 갤러리 이미지를 사용할 수 있다.

blog_page.html을 수정해보자. 

{% extends "base.html" %}

{% 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: left; margin: 10px">
            {% image item.image fill-320x240 %}
            <p>{{ item.caption }}</p>
        </div>
    {% endfor %}

    <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>

{% endblock %}

<img> 요소를 넣기 위해 {% image %} 태그를 사용하고 있는데 wagtailimages_tags에 정의 되어 있기 때문에 템플릿 상단에 이를 import 해줘야한다. fill-320x240로 이미지를 조정할 수 있다. (fill은 사각형을 채워준다.)

 

이미지는 자체가 객체이기 때문에 다른 곳에서도 조회하고 사용이 가능하다.

blog/models.py 를 수정해보자.

class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    def main_image(self):
        gallery_item = self.gallery_images.first()
        if gallery_item:
            return gallery_item.image
        else:
            return None

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

새로 정의한 메소드를 사용해보자 게시물의 이미지 썸네일로 사용해볼 것이다.

base_index_page.html를 수정하자

{% 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 %}

 게시물 태그 기능

태그 기능을 추가해 볼 것이다. wagtail의 태그 기능을 가져와서 BlogPage 모델과 content panel에 붙이고 blog_post 템플릿에 렌더링하면 된다. 

 

blog/models.py 를 수정하자

from django.db import models

# New imports added for ClusterTaggableManager, TaggedItemBase, MultiFieldPanel

from modelcluster.fields import ParentalKey
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase

from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index


# ... (Keep the definition of BlogIndexPage)


class BlogPageTag(TaggedItemBase):
    content_object = ParentalKey(
        'BlogPage',
        related_name='tagged_items',
        on_delete=models.CASCADE
    )


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)

    # ... (Keep the main_image method and search_fields definition)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

modelcluster, taggit 을 import 해야한다. BlogPageTag 모델을 추가하고 BlogPage 모델에는 tags 필드를 추가했다. 

content_panels 에서 MultiFieldPanel을 사용하면 그룹화가 가능해서 가독성이 좋아진다.

 

blog_page.html 아래에 다음 내용을 추가하자

{% if page.tags.all.count %}
    <div class="tags">
        <h3>Tags</h3>
        {% for tag in page.tags.all %}
            <a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
        {% endfor %}
    </div>
{% endif %}

여기서는 pageurl 대신 slugurl을 사용했는데 slugurl은 Promote에서 설정한 slug 값을 가져온다는 것이다. 

pageurl은 명확하고 추가적인 데이터베이스 조회를 피할 수 있지만 지금같은 경우에는 페이지 객체를 쉽게 구할 수 없어 slug tag를 사용한다.

 

태그를 통해 접속하면 아직 view를 정의하지 않았기 때문에 404가 나올 것이다.

blog/models.py를 수정해보자.

class BlogTagIndexPage(Page):

    def get_context(self, request):

        # Filter by tag
        tag = request.GET.get('tag')
        blogpages = BlogPage.objects.filter(tags__name=tag)

        # Update template context
        context = super().get_context(request)
        context['blogpages'] = blogpages
        return context

Page를 기반으로 한 모델 BlogTagIndexPage는 자체 필드를 정의하지는 않는다. 필드가 없어도 wagtail Page하위 클래스로 만들어지기 때문에 get_context() 메소드에서 QuerySet으로 내용을 조작할 수 있다.

 

makemigrations를 진행 한뒤, admin 페이지에서 HomePage의 자식으로 tag라는 슬러그를 부여해주면 된다.

/tags로 접속하면 오류가 뜰텐데 아직 blog_tag_index_page.html이 없어서 그렇다 추가해주면된다.

{% extends "base.html" %}
{% load wagtailcore_tags %}

{% block content %}

    {% if request.GET.tag|length %}
        <h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
    {% endif %}

    {% for blogpage in blogpages %}

          <p>
              <strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
              <small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
              {% if blogpage.author %}
                <p>By {{ blogpage.author.profile }}</p>
              {% endif %}
          </p>

    {% empty %}
        No pages found with that tag.
    {% endfor %}

{% endblock %}

위 코드를 보면 blogpage.latest_revision_created_at 필드를 호출하는 부분이 있는데 내장 필드이기 때문에 페이지 인스턴스라면 호출 할 수 있다.

 

카테고리

 

카테고리를 추가해보자. 카테고리는 page가 아니기 때문에 표준 장고 model.Model로 정의 한다. wagtail에는 존재하지 않는 재사용 가능한 컨텐츠를 위해 "snippets"개념을 소개한다.

 

@register_snippet 처럼 데코레이터를 이용하여 등록할 수 있다. 지금까지 페이지에서 사용한 모든 필드 유형도 snippet에서도 사용할 수 있다. 각 카테고리에 이름과 아이콘 이미지를 제공한다. blog/models.py에 추가하자. 

from wagtail.snippets.models import register_snippet


@register_snippet
class BlogCategory(models.Model):
    name = models.CharField(max_length=255)
    icon = models.ForeignKey(
        'wagtailimages.Image', null=True, blank=True,
        on_delete=models.SET_NULL, related_name='+'
    )

    panels = [
        FieldPanel('name'),
        ImageChooserPanel('icon'),
    ]

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'blog categories'

기존과 다르게 content_panels 대신에 panels를 사용하고 있는데 sinppet은 slug 또는 date와 같은 필드가 필요하지 않으므로 일반적으로 admin에서 제공하는 content, promote, settings 탭으로 분할되지 않으므로 panel 또한 content_panel과 promote_panel로 구별될 필요가 없다. 따라서 그냥 panel을 쓰고 있다.

 

makemigrations를 진행하고 admin 페이지 메뉴에 나타나는 snippets 을 통해 카테고리 몇개를 추가하자.

 

이제 BlogPage 모델에 manytomany 로 카테고리를 추가할 수 있다. 카테고리 사용필드는 ParetnalManyToManyField이다. 표준 ManytoMany 필드의 변형이라고 보면 된다. blog/models.py를 수정해주자. 

# New imports added for forms and ParentalManyToManyField
from django import forms
from django.db import models

from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase

# ...

class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    categories = ParentalManyToManyField('blog.BlogCategory', blank=True)

    # ... (Keep the main_image method and search_fields definition)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
            FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

FieldPanel의 옵션값을 보면 사용자 친화적인 CheckBoxSelect로 설정했다.

 

카테고리 표시를 위해 blog_page.html 을 수정해주자.

<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>

{% with categories=page.categories.all %}
    {% if categories %}
        <h3>Posted in:</h3>
        <ul>
            {% for category in categories %}
                <li style="display: inline">
                    {% image category.icon fill-32x32 style="vertical-align: middle" %}
                    {{ category.name }}
                </li>
            {% endfor %}
        </ul>
    {% endif %}
{% endwith %}

이것으로 기본 tutorial은 완료 했다. /blog로 보면 지금동안 기술했던 내용이 잘 보일 것이다. 

'Back-End > Wagtail, Django' 카테고리의 다른 글

Wagtail demo site (breads)  (0) 2021.08.17
Wagtail demo site (blog)  (0) 2021.08.17
Wagtail demo site (base)  (0) 2021.08.15
Wagtail 이미지  (0) 2021.08.13
Wagtail StreamField  (0) 2021.08.11

+ Recent posts