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

 

Wagtail의 demo site를 분석해보려고 한다. 

 

https://github.com/wagtail/bakerydemo

 

bakerydemo 라는 사이트인데 문서에 따르면 설치방법을 5가지를 소개하고 있다. 

Vagrant나 Docker를 추천하고 있어서 Docker로 실행해보려고 한다. 

git clone https://github.com/wagtail/bakerydemo.git
cd bakerydemo
docker-compose up --build -d
docker-compose run app /venv/bin/python manage.py load_initial_data
docker-compose up

참고로 build가 오래 걸린다. 인내심을 가지고 기다려보자.

compose up까지 실행하면 http://localhost:8000/ 로 접속할 수 있을것이다.

http://localhost:8000/admin/

으로 접속하자 admin / changeme

(아이디 / 비밀번호)

 

bakery demo의 구조를 살펴보자. 

 

bakery demo 구조

일단 base, blog, breads, locations, media, sarch, settings, static, templates 폴더를 볼 수가 있다. 

 

base

base폴더 부터 살펴보자. base는 제일 기본이 되는 폴더이다.

base 구조

먼저 Wagtail에서 제일 중요한 models.py부터 살펴 보겠다. 코드가 상당히 길어서 부분으로 살펴보도록 하겠다. 

class를 기준으로 각 모델들을 살펴 보려고 한다. 

 

models.py

from __future__ import unicode_literals

from django.db import models

from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel

from wagtail.admin.edit_handlers import (
    FieldPanel,
    FieldRowPanel,
    InlinePanel,
    MultiFieldPanel,
    PageChooserPanel,
    StreamFieldPanel,
)
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Collection, Page
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index
from wagtail.snippets.models import register_snippet

from .blocks import BaseStreamBlock

먼저 최상단 module import 부분이다. 관련해서는 마지막으로 어떤 모듈이 어떤 기능을 하는지 정리해보는 시간을 갖을 예정이다.

 

class People

@register_snippet
class People(index.Indexed, ClusterableModel):

    first_name = models.CharField("First name", max_length=254)
    last_name = models.CharField("Last name", max_length=254)
    job_title = models.CharField("Job title", max_length=254)

    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    panels = [
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('first_name', classname="col6"),
                FieldPanel('last_name', classname="col6"),
            ])
        ], "Name"),
        FieldPanel('job_title'),
        ImageChooserPanel('image')
    ]

    search_fields = [
        index.SearchField('first_name'),
        index.SearchField('last_name'),
    ]

    @property
    def thumb_image(self):
        # Returns an empty string if there is no profile pic or the rendition
        # file can't be found.
        try:
            return self.image.get_rendition('fill-50x50').img_tag()
        except:  # noqa: E722 FIXME: remove bare 'except:'
            return ''

    def __str__(self):
        return '{} {}'.format(self.first_name, self.last_name)

    class Meta:
        verbose_name = 'Person'
        verbose_name_plural = 'People'

People 모델이다. 사람 객체를 저장하는 Django model이다. 

snippet을 사용해서 관리하도록 했다. 굳이 page를 통해 보여줄 필요가 없지만 CRUD가 필요한 객체들은 snippet을 통해 관리하는 것으로 보여진다. 

 

snippet

실제로 admin에서 snippet을 보게 되면 빵의 재료, 빵 종류, 원산지, footer text, people로 이루어져 있는 것을 확인 할 수 있다. 이들 객체는 snippet을 통해 쉽게 관리되어 진다. tutorial에서도 카테고리는 snippet을 통해 관리되어 지는 것을 봤었다. 굳이 따로 page를 구성하여 관리할 필요가 없다면 snippet을 통해 관리하는 것이 효과적일 것이다. 

 

다시 people을 분석해보자. 

people에서는 ClusterableModel을 사용하고 있다. ClusterableModel은 다른 모델들이 각자의 부모 모델에 명시적으로 저장될때까지 relationship을 구성할 수 있도록 해준다. 즉, 이 모듈은 우리가 preview 버튼을 사용할 수 있게 해주는데 관련 content의 relationship을 데이터 베이스에 저장없이 preview 할 수 있게 한다. 

 

people의 기본 구성은 first_name, last_name, job_title, image 필드로 구성되어 있다. 

panels에서는 이름란에 RowPanel을 통해 그룹화 한 것을 볼 수 있다. classname을 통하여 각 어트리뷰트의 위치를 조정 하였다. 

 

@property 기능은 @property참고

self.image.get_rendition('fill-50x50').img_tag()

Wagtail에서 사진을 원하는 사이즈나 퀄리티로 coustom 할 수 있게 해줄 수 있다. 

newimage = myimage.get_rendition('fill-300x150|jpegquality-60')

<사용예제>

자세한 내용이 알고 싶다면 renditions참고

 

class FooterText

@register_snippet
class FooterText(models.Model):
   
    body = RichTextField()

    panels = [
        FieldPanel('body'),
    ]

    def __str__(self):
        return "Footer text"

    class Meta:
        verbose_name_plural = 'Footer Text'

이부분은 bakery demo의 footer부분의 텍스트를 바꿀 수 있게 해준다. 역시 snippet을 통해 등록해서 코드 수정없이 바꿀 수 있게 설정을 해놨다. 

 

class StandardPage

 

class StandardPage(Page):

    introduction = models.TextField(
        help_text='Text to describe the page',
        blank=True)
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Landscape mode only; horizontal width between 1000px and 3000px.'
    )
    body = StreamField(
        BaseStreamBlock(), verbose_name="Page body", blank=True
    )
    content_panels = Page.content_panels + [
        FieldPanel('introduction', classname="full"),
        StreamFieldPanel('body'),
        ImageChooserPanel('image'),
    ]

Standard page는 일반적인 content page이다. 여기서는 about page로 사용했지만 만약에 제목, 사진, 소개 body field 정도만 필요하다면 아무 곳에서나 사용할 수 있다.

 

class Homepage

 

class HomePage(Page):
    """
    The Home Page. This looks slightly more complicated than it is. You can
    see if you visit your site and edit the homepage that it is split between
    a:
    - Hero area
    - Body area
    - A promotional area
    - Moveable featured site sections
    """

    # Hero section of HomePage
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Homepage image'
    )
    hero_text = models.CharField(
        max_length=255,
        help_text='Write an introduction for the bakery'
    )
    hero_cta = models.CharField(
        verbose_name='Hero CTA',
        max_length=255,
        help_text='Text to display on Call to Action'
    )
    hero_cta_link = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name='Hero CTA link',
        help_text='Choose a page to link to for the Call to Action'
    )

    # Body section of the HomePage
    body = StreamField(
        BaseStreamBlock(), verbose_name="Home content block", blank=True
    )

    # Promo section of the HomePage
    promo_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Promo image'
    )
    promo_title = models.CharField(
        null=True,
        blank=True,
        max_length=255,
        help_text='Title to display above the promo copy'
    )
    promo_text = RichTextField(
        null=True,
        blank=True,
        help_text='Write some promotional copy'
    )

    # Featured sections on the HomePage
    # You will see on templates/base/home_page.html that these are treated
    # in different ways, and displayed in different areas of the page.
    # Each list their children items that we access via the children function
    # that we define on the individual Page models e.g. BlogIndexPage
    featured_section_1_title = models.CharField(
        null=True,
        blank=True,
        max_length=255,
        help_text='Title to display above the promo copy'
    )
    featured_section_1 = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='First featured section for the homepage. Will display up to '
        'three child items.',
        verbose_name='Featured section 1'
    )

    featured_section_2_title = models.CharField(
        null=True,
        blank=True,
        max_length=255,
        help_text='Title to display above the promo copy'
    )
    featured_section_2 = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Second featured section for the homepage. Will display up to '
        'three child items.',
        verbose_name='Featured section 2'
    )

    featured_section_3_title = models.CharField(
        null=True,
        blank=True,
        max_length=255,
        help_text='Title to display above the promo copy'
    )
    featured_section_3 = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Third featured section for the homepage. Will display up to '
        'six child items.',
        verbose_name='Featured section 3'
    )

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            ImageChooserPanel('image'),
            FieldPanel('hero_text', classname="full"),
            MultiFieldPanel([
                FieldPanel('hero_cta'),
                PageChooserPanel('hero_cta_link'),
            ]),
        ], heading="Hero section"),
        MultiFieldPanel([
            ImageChooserPanel('promo_image'),
            FieldPanel('promo_title'),
            FieldPanel('promo_text'),
        ], heading="Promo section"),
        StreamFieldPanel('body'),
        MultiFieldPanel([
            MultiFieldPanel([
                FieldPanel('featured_section_1_title'),
                PageChooserPanel('featured_section_1'),
            ]),
            MultiFieldPanel([
                FieldPanel('featured_section_2_title'),
                PageChooserPanel('featured_section_2'),
            ]),
            MultiFieldPanel([
                FieldPanel('featured_section_3_title'),
                PageChooserPanel('featured_section_3'),
            ]),
        ], heading="Featured homepage sections", classname="collapsible")
    ]

    def __str__(self):
        return self.title

여긴 주석을 지우지 않았다. 홈페이지를 section으로 분리하여 각각 부분을 수정할 수 있게 만들었다. 

디자인은 유지하지만 page의 content나 image는 전부 admin page에서 수정할 수 있도록 하는 것이 핵심이다.

여기서 처음 본 것은 PageChooserPanel인데 a태그로 설정해주었던 링크도 panel을 통해서 쉽게 설정할 수 있었다. 

 

여기서 제일 눈여겨 보아야 할 것은 child page의 객체로 바로 이동할 수 있도록 구현한 것인데 demo site의 각각의 blog site, locations breads로 바로 이동할 수 있도록 각 필드는 ForeignKey로 작성하였다.

 

home_page.html의 코드를 보면 이해하기가 쉽다. 

<div class="col-sm-6 col-sm-offset-1 feature-1">
            {% if page.featured_section_1 %}
            <h2>{{ page.featured_section_1_title }}</h2>
                <div class="featured-children">
                    {% for childpage in page.featured_section_1.specific.children|slice:"4" %}
                        <li>
                            <div class="row">
                                <div class="col-xs-4">
                                    <a href="{{childpage.url}}">
                                        <figure>
                                            {% image childpage.image fill-180x140-c100 %}
                                        </figure>
                                    </a>
                                </div>
                                <div class="col-xs-8">
                                    <h3><a href="{{childpage.url}}">{{childpage.title}}</a></h3>
                                </div>
                            </div>
                        </li>
                    {% endfor %}
                </div>
            {% endif %}
        </div>

선택한 childpage의 children을 가져와서 표시를 해주는 것이다. slice: "4"는 4개 까지만 표시하라는 의미이다. Wagtail은 tree 구조로 되어 있다. 지금 demo site에서는 상위 요소 homepage 아래에 blog, breads, locations 페이지가 children으로 존재하고 각각의 세부 페이지에서 또 children들이 존재한다. 

 

class GalleryPage

 

class GalleryPage(Page):
  
    introduction = models.TextField(
        help_text='Text to describe the page',
        blank=True)
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        help_text='Landscape mode only; horizontal width between 1000px and '
        '3000px.'
    )
    body = StreamField(
        BaseStreamBlock(), verbose_name="Page body", blank=True
    )
    collection = models.ForeignKey(
        Collection,
        limit_choices_to=~models.Q(name__in=['Root']),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        help_text='Select the image collection for this gallery.'
    )

    content_panels = Page.content_panels + [
        FieldPanel('introduction', classname="full"),
        StreamFieldPanel('body'),
        ImageChooserPanel('image'),
        FieldPanel('collection'),
    ]
    subpage_types = []

새로운 개념이 나오는 GalleryPage이다. Collection이라는 것을 사용했는데 간단히 말해서 Collection은 image의 category라고 생각하면 된다. admin page에 Settings에 Collection을 생성할 수 있다. 그러면 앞으로 image를 업로드 할때 내가 생성한 collection을 선택할 수 있을 것이다. Q object라는 개념도 나오는데 이것은 쉽게 말하면 django orm에 where절의 조건을 입력할 수 있도록 해주는 것이다. 관련 내용을 더 알고 싶다면 아래 링크를 참조하자

Collection

Q,F object

 

class FormField, FormPage

 

class FormField(AbstractFormField):
    
    page = ParentalKey('FormPage', related_name='form_fields', on_delete=models.CASCADE)


class FormPage(AbstractEmailForm):
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    body = StreamField(BaseStreamBlock())
    thank_you_text = RichTextField(blank=True)

    # Note how we include the FormField object via an InlinePanel using the
    # related_name value
    content_panels = AbstractEmailForm.content_panels + [
        ImageChooserPanel('image'),
        StreamFieldPanel('body'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text', classname="full"),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

Wagtail의 FormPage이다. demo site에서는 Contact us의 Form field를 만들 때, 사용했다. Wagtailforms 는 간단한 form을 소개해주는 module이다. 이것은 Django form을 대체하도록 의도하지는 않았지만 코드의 작성없이 빠르게 data form을 만들 수 있게 해준다. 

class Fromfield의 ParentalKey는 Django의 ForeignKey의 하위 클래스로 module을 사용할 수 있게 해준다. 여기서는 AbstractFormField를 사용할 수 있게 해준다.

AbstractEmailForm은 to_address, from_address and subject을 제공해주는데 그냥 AbstractFormField를 통해 정의 해도 된다. InlinePanel을 통해 form_fields를 추가 해주었고 

Form document 

Form에 관한건 관련 링크를 통해 좀 더 살펴볼 수 있다.

 

templatetag

templatetags에는 gallery_tags,py 와 navigation_tags.py를 볼 수 있다.

gallery_tags.py

from django import template

from wagtail.images.models import Image

register = template.Library()


# Retrieves a single gallery item and returns a gallery of images
@register.inclusion_tag('tags/gallery.html', takes_context=True)
def gallery(context, gallery): 
    images = Image.objects.filter(collection=gallery)

    return {
        'images': images,
        'request': context['request'],
    }

태그를 구현하려면 상당히 복잡하지만 Django 에서는 태그를 쉽게 구현할 수 있게 도와준다. 

바로 register를 사용하는 방법이다. template 라이브러리를 register 변수에 넣고 

@register.inclusion_tag(태그 템플릿 주소, takes_context=True) 형식으로 만들고 아래는 원하는 tag 페이지를 출력하는 orm을 적어주면 된다. 

Wagtail기능이 아니라 Django의 기능이다. 자세한건 아래 링크에서 확인 할 수 있다.

Dajngo custom Tag

 

navigation.py

 

wagtail_hooks.py

from wagtail.contrib.modeladmin.options import (
    ModelAdmin, ModelAdminGroup, modeladmin_register)

from bakerydemo.breads.models import Country, BreadIngredient, BreadType
from bakerydemo.base.models import People, FooterText

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


class BreadIngredientAdmin(ModelAdmin):
    model = BreadIngredient
    search_fields = ('name', )


class BreadTypeAdmin(ModelAdmin):
    model = BreadType
    search_fields = ('title', )


class BreadCountryAdmin(ModelAdmin):
    model = Country
    search_fields = ('title', )


class BreadModelAdminGroup(ModelAdminGroup):
    menu_label = 'Bread Categories'
    menu_icon = 'fa-suitcase'  # change as required
    menu_order = 200  # will put in 3rd place (000 being 1st, 100 2nd)
    items = (BreadIngredientAdmin, BreadTypeAdmin, BreadCountryAdmin)


class PeopleModelAdmin(ModelAdmin):
    model = People
    menu_label = 'People'  # ditch this to use verbose_name_plural from model
    menu_icon = 'fa-users'  # change as required
    list_display = ('first_name', 'last_name', 'job_title', 'thumb_image')
    list_filter = ('job_title', )
    search_fields = ('first_name', 'last_name', 'job_title')


class FooterTextAdmin(ModelAdmin):
    model = FooterText
    search_fields = ('body',)


class BakeryModelAdminGroup(ModelAdminGroup):
    menu_label = 'Bakery Misc'
    menu_icon = 'fa-cutlery'  # change as required
    menu_order = 300  # will put in 4th place (000 being 1st, 100 2nd)
    items = (PeopleModelAdmin, FooterTextAdmin)

modeladmin_register(BreadModelAdminGroup)
modeladmin_register(BakeryModelAdminGroup)

wagtail_hook.py는 admin 페이지의 UI를 수정하는 코드이다. 

일단 icon을 사용하기 위해 styleguide를 settings의 base.py에 정의 해 주었다. 

model은 custom하고 싶은 모델 클래스를 정의해주면 된다.

search_fields는 모델의 검색 attribute를 정의해주면된다.

menu_label은 메뉴에서 보여질 이름을 정하면 된다.

menu_icon은 아이콘을 정의해주면 되는데 bakery demo에서는 이미 base.html에 font-awsome을 사용할 수 있게 설정해 놓았다. font-awsome에서 원하는 아이콘을 가지고 설정하면 된다.

그룹을 설정하고 싶으면 ModelAdminGroup을 사용하면 된다. 

그리고 최정적으로 admin에 정의한 class들을 등록해주면 된다. (Django admin 설정이랑 같음)

 

blcoks.py

from wagtail.images.blocks import ImageChooserBlock
from wagtail.embeds.blocks import EmbedBlock
from wagtail.core.blocks import (
    CharBlock, ChoiceBlock, RichTextBlock, StreamBlock, StructBlock, TextBlock,
)


class ImageBlock(StructBlock):
    """
    Custom `StructBlock` for utilizing images with associated caption and
    attribution data
    """
    image = ImageChooserBlock(required=True)
    caption = CharBlock(required=False)
    attribution = CharBlock(required=False)

    class Meta:
        icon = 'image'
        template = "blocks/image_block.html"


class HeadingBlock(StructBlock):
    """
    Custom `StructBlock` that allows the user to select h2 - h4 sizes for headers
    """
    heading_text = CharBlock(classname="title", required=True)
    size = ChoiceBlock(choices=[
        ('', 'Select a header size'),
        ('h2', 'H2'),
        ('h3', 'H3'),
        ('h4', 'H4')
    ], blank=True, required=False)

    class Meta:
        icon = "title"
        template = "blocks/heading_block.html"


class BlockQuote(StructBlock):
    """
    Custom `StructBlock` that allows the user to attribute a quote to the author
    """
    text = TextBlock()
    attribute_name = CharBlock(
        blank=True, required=False, label='e.g. Mary Berry')

    class Meta:
        icon = "fa-quote-left"
        template = "blocks/blockquote.html"


# StreamBlocks
class BaseStreamBlock(StreamBlock):
    """
    Define the custom blocks that `StreamField` will utilize
    """
    heading_block = HeadingBlock()
    paragraph_block = RichTextBlock(
        icon="fa-paragraph",
        template="blocks/paragraph_block.html"
    )
    image_block = ImageBlock()
    block_quote = BlockQuote()
    embed_block = EmbedBlock(
        help_text='Insert an embed URL e.g https://www.youtube.com/embed/SGJFWirQ3ks',
        icon="fa-s15",
        template="blocks/embed_block.html")

models.py에서 사용할 stream 블록을 정의 해놓은 곳이다. image, heading, quote를 정의 해놓고 BaseStreamBlock 클래스에 블록을 정의하고 실제 models.py에서는 BaseStreamBlock만 정의 해 놓았다. 

 

나머지 폴더를 살펴보자면

fixture에는 demosite에 사용한 사진이 들어있다. 

 

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

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

+ Recent posts