https://studiomeal.com/archives/197

정말 친절하게도 wagtail projcet를 시작하면 Dockerfile이 이미 준비되어 있다. 굳이 만들필요가 없고 간단한 명령어 몇개만으로 이용할 수 있다.

 

docker build -t mysite .
docker run -p 8000:8000 mysite

mysite에서는 자신의 project 이름을 적어주면 된다.

 

docker build를 진행하였으면 docker hub에도 올려보자. 

https://hub.docker.com/

도커 hub에 접속하고 로그인한다. 

Create Repository를 하고 wagtail 프로젝트와 같은 이름으로 설정하자.

만들고 나면 docker command라는 것이 있다. 

 

push를 하기전에 일단 docker image 설정부터 해줘야한다.

명령어 실행기에 dockerlogin을 입력하고 로그인을 해준다.

 

docker images 를 입력하면 우리가 만든 docker image들이 있다. 

docker hub에 push를 해주려면 고유의 이름으로 변환해줄 필요가 있다. 

 

이미지 파일 이름과 tag를 잘 기억해보자. 

그리고 명령어에

docker tag name:tag (내 아이디)(내 레포지토리)/name:tag를 입력해준다.

 

docker images를 입력하여 제대로 생성이 되었으면

아까 docker command를 붙여넣고 tagname만 수정해주면 성공이다. 

 

모든 도커 컨테이너 삭제(remove all docker containers)

구동중인 모든 도커 컨테이너들을 중지시키고, 삭제한다.

 docker stop $(docker ps -a -q)
 docker rm $(docker ps -a -q)

모든 도커 이미지 삭제(remove all docker images)

 docker rmi $(docker images -q) 

기본문법

1) image id를 이용하여 삭제

docker rmi IMAGE_ID

2) repository와 tag를 이용하여 삭제

docker rmi REPOSITORY:TAG

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

Wagtail Custom User Field  (0) 2021.09.01
Wagtail All Auth(Google Login)  (0) 2021.08.25
Wagtail demo site (breads)  (0) 2021.08.17
Wagtail demo site (blog)  (0) 2021.08.17
Wagtail demo site (base)  (0) 2021.08.15

이번엔 breads app을 살펴볼 예정이다. 이곳은 paginate 기능 이외에는 특별한 것이 없다. 그래서 paginate만 분석해보고 넘어갈 생각이다. 

 

# Returns a queryset of BreadPage objects that are live, that are direct
    def get_breads(self):
        return BreadPage.objects.live().descendant_of(
            self).order_by('-first_published_at')

    def children(self):
        return self.get_children().specific().live()

    def paginate(self, request, *args):
        page = request.GET.get('page')
        paginator = Paginator(self.get_breads(), 12)
        try:
            pages = paginator.page(page)
        except PageNotAnInteger:
            pages = paginator.page(1)
        except EmptyPage:
            pages = paginator.page(paginator.num_pages)
        return pages

    def get_context(self, request):
        context = super(BreadsIndexPage, self).get_context(request)

        # BreadPage objects (get_breads) are passed through pagination
        breads = self.paginate(request, self.get_breads())

        context['breads'] = breads

        return context

Wagtail에서 paginate기능을 제공해준다. 우리는 가져다가 사용하기만 하면 된다.

 

일단

def get_breads(self):
        return BreadPage.objects.live().descendant_of(
            self).order_by('-first_published_at')

로 breadpage의 object들을 전부 가져온다. descendant_of는 현재 Class 인 BreadIndexPage의 자식요소 즉 BlogPage를 가르킨다. 즉 해석하면 이 BreadIndexpage의 BreadPage의 publish된 object들을 시간순으로 전부 가져오라는 뜻이다. 

 

def children(self):
        return self.get_children().specific().live()

은 blogpage의 attribute를 사용하기 위해 children으로 정의를 해 놓았다.

 

일단 

page = request.GET.get('page') 로 현재의 page를 GET 해온다. 그리고 Paginator를 통해 get_breads()에서 정의한 breadpages을 12개 기준으로 가져온다. 

paginator의 page attribute에 page를 담고 return한다. 

이제 get_context method를 통해 breads라는 이름으로 template에 전달한다. 

 

정리하자면 BreadIndexPage에 있는 BreadPage들을 가져와서 12개씩 paginate를 하고 전달해주는 방식이다. 관련한 template은 include/pagination.html에 작성되어 있다. 

 

 

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

Wagtail All Auth(Google Login)  (0) 2021.08.25
Wagtail Docker 사용 방법  (0) 2021.08.18
Wagtail demo site (blog)  (0) 2021.08.17
Wagtail demo site (base)  (0) 2021.08.15
Wagtail 이미지  (0) 2021.08.13

저번 시간에 이어서 이번에는 blog app을 분석해보려고 한다. 코드를 전체 다루지 않고 base app에서 다루지 않았던 새로운 내용만 적어볼 생각이다.

models.py

class BlogPeopleRelationship(Orderable, models.Model):
 
    page = ParentalKey(
        'BlogPage', related_name='blog_person_relationship', on_delete=models.CASCADE
    )
    people = models.ForeignKey(
        'base.People', related_name='person_blog_relationship', on_delete=models.CASCADE
    )
    panels = [
        SnippetChooserPanel('people')
    ]

 

base app에 models.py에 정의 했던 people을 여기서 사용하게 된다. 

ParentalKey을 이용해서 어떤 모델에 연관 시킬건지를 정한다. 여기서는 BlogPage에서 사용하려고 한다.

ForeignKey에서는 base app에서 정의 해 놓은 People 클래스를 가져온다. 

People 클래스를 이미 snippet으로 정의를 해 놓았으니 여기도 panel은 snippet을 사용한다. 

 

class Blog

class BlogPage(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
    )
    subtitle = models.CharField(blank=True, max_length=255)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    date_published = models.DateField(
        "Date article published", blank=True, null=True
    )

    content_panels = Page.content_panels + [
        FieldPanel('subtitle', classname="full"),
        FieldPanel('introduction', classname="full"),
        ImageChooserPanel('image'),
        StreamFieldPanel('body'),
        FieldPanel('date_published'),
        InlinePanel(
            'blog_person_relationship', label="Author(s)",
            panels=None, min_num=1),
        FieldPanel('tags'),
    ]

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

    def authors(self):
        authors = [
            n.people for n in self.blog_person_relationship.all()
        ]

        return authors

    @property
    def get_tags(self):
        tags = self.tags.all()
        for tag in tags:
            tag.url = '/' + '/'.join(s.strip('/') for s in [
                self.get_parent().url,
                'tags',
                tag.slug
            ])
        return tags

    parent_page_types = ['BlogIndexPage']

    subpage_types = []

tags = ClusterTaggableManager(through=BlogPageTag, blank=True) 에서는 BlogPageTag을 통해 tags를 정의 해주고 있다. 잠시 BlogPageTag를 살펴 보자면 

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

 

ParentalKey을 사용해서 BlogPage의 자식이라는 것을 정의해 놓는다. 이렇게 되면 BlogPage와 manytomany 관계를 생성하도록 해준다. 

 

def author와 def get_tags 를 살펴보도록 하겠다. 

둘다 비슷한 기능을 하는데 여러명의 authors(people)와 여러개의 tags들을 templates에서 표시해줄 수 있게 미리 정의를 해놓았다. template에서 authors 와 get_tags를 사용하게 되면 쉽게 author 와 tag들을 표시할 수 있다. 

 

<div class="blog-meta">
                    {% if page.authors %}
                        <div class="blog-avatars">
                            {% for author in page.authors %}
                                <div class="author">{% image author.image fill-50x50-c100 class="blog-avatar" %}
                                    {{ author.first_name }} {{ author.last_name }}</div>
                            {% endfor %}
                        </div>
                    {% endif %}

                    {% if page.date_published %}
                        <div class="blog-byline">
                            {{ page.date_published }}
                        </div>
                    {% endif %}
                </div>

                {{ page.body }}

                {% if page.get_tags %}
                    Tagged with:<br />
                    {% for tag in page.get_tags  %}
                        <a href="{{ tag.url }}" class="btn btn-sm">{{ tag }}</a>
                    {% endfor %}
                {% endif %}

실제로 blog_page.html 을 살펴보게 되면 blogpage와 관련된 사람이나 태그를 표시해주는 것을 확인 할 수 있다. 

parent_page_types = ['BlogIndexPage'] 을 정의하여 blogpage가 blogindexpage의 자식 page임을 알려준다.

 

class BlogIndexPage

class BlogIndexPage(RoutablePageMixin, 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.'
    )

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

    subpage_types = ['BlogPage']

    def children(self):
        return self.get_children().specific().live()

    def get_context(self, request):
        context = super(BlogIndexPage, self).get_context(request)
        context['posts'] = BlogPage.objects.descendant_of(
            self).live().order_by(
            '-date_published')
        return context

    @route(r'^tags/$', name='tag_archive')
    @route(r'^tags/([\w-]+)/$', name='tag_archive')
    def tag_archive(self, request, tag=None):

        try:
            tag = Tag.objects.get(slug=tag)
        except Tag.DoesNotExist:
            if tag:
                msg = 'There are no blog posts tagged with "{}"'.format(tag)
                messages.add_message(request, messages.INFO, msg)
            return redirect(self.url)

        posts = self.get_posts(tag=tag)
        context = {
            'tag': tag,
            'posts': posts
        }
        return render(request, 'blog/blog_index_page.html', context)

    def serve_preview(self, request, mode_name):
        # Needed for previews to work
        return self.serve(request)

    def get_posts(self, tag=None):
        posts = BlogPage.objects.live().descendant_of(self)
        if tag:
            posts = posts.filter(tags=tag)
        return posts

    def get_child_tags(self):
        tags = []
        for post in self.get_posts():
            # Not tags.append() because we don't want a list of lists
            tags += post.get_tags
        tags = sorted(set(tags))
        return tags

subpage_types = ['BlogPage'] 는 BlogIndexPage의 자식 Page는 오로지 BlogPage임을 정의해준다.

 

def children(self):
        return self.get_children().specific().live() 을 통해 현재 publish 된 page만 돌려주도록 설정해 놓는다. 

demo site에서는 homepage에서 사용하기 위해서 설정해 놓았다.

 

blogindexpage에서는 get_context을 통해 posts 라는 이름으로 template에 전달하였다.

@route(r'^tags/$', name='tag_archive')
    @route(r'^tags/([\w-]+)/$', name='tag_archive')
    def tag_archive(self, request, tag=None):

        try:
            tag = Tag.objects.get(slug=tag)
        except Tag.DoesNotExist:
            if tag:
                msg = 'There are no blog posts tagged with "{}"'.format(tag)
                messages.add_message(request, messages.INFO, msg)
            return redirect(self.url)

        posts = self.get_posts(tag=tag)
        context = {
            'tag': tag,
            'posts': posts
        }
        return render(request, 'blog/blog_index_page.html', context)

유의 깊게 살펴볼 부분이 이곳이다. 이 함수는 Tag를 보여주는 Custom view이다. 이 View는 연관되어 있는 Tag들을 전부 주고 blogindexpage redirect를 가능하게 해준다. 

@route 를 사용하여 tags 자체 url를 설정해준다. name에 관련 내용을 정의한 함수를 적어준다. 여기서는 tag_archive라는 이름으로 정의 했다.

blogindexpage를 보게 되면 위와 같이 등록된 tag들이 있는 것을 볼 수 있다. 각각의 태그를 누르면 태그에 연관되어 있는 blog들만 나오게 된다. 

전체적인 코드의 흐름을 살펴보자면 tag page의 router를 custom 해주고 tag변수에 Tag object들을 저장한다. tag에는 None으로 초기화를 해주면 된다. 오류 검사를 진행한 다음 

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

Wagtail Docker 사용 방법  (0) 2021.08.18
Wagtail demo site (breads)  (0) 2021.08.17
Wagtail demo site (base)  (0) 2021.08.15
Wagtail 이미지  (0) 2021.08.13
Wagtail StreamField  (0) 2021.08.11

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

wagtail의 이미지 처리에 대해서 공부해보려고 한다.

 

models.py

class CardPage(Page):
    date = models.DateField("Card date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    categories = ParentalManyToManyField('card.CardCategory', blank=True)

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

    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 + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
        ], heading="Card information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]


class CardPageGalleryImage(Orderable):
    page = ParentalKey(CardPage, 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'),
    ]

위와 같은 Model이 있다고 해보자. CardPageGalleryImage는 Orderable로 CardPage에서 여러개를 추가할 수 있는 Clusterable 모델이다. 

 

def main_image에서는 여러개의 갤러리 이미지 중, 첫번 째 이미지만 가져와서 return 해준다. 

 

 

card_index_page.html

{% load wagtailimages_tags %}

<div class="container pt-3 pb-3">
                <div class="card" style="width: 18rem; text-align: center;">
                    {% image card.main_image width-400 as tmp_photo %}
                    <img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}" height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />
                </div>
            {% endwith %}
        {% endfor %}
    </div>

간단하게 image를 어떻게 template에서 표현하는지 살펴보자.

Wagtail에서는 template에 image를 사용할 때 {% load wagtailimages_tags %}를 상단에 추가해줘야한다. 

그리고 {% image [image] [resize-rule] as '줄임말' %} 형식으로 작성한다. 

'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 StreamField  (0) 2021.08.11
Wagtail tutorial (튜토리얼)  (0) 2021.08.11

+ Recent posts