사용자 정의 StreamField 블록을 만드는 방법

StructBlock 에 대한 사용자 정의 편집 인터페이스

페이지 편집기에 나타나는 StructBlock 의 스타일을 사용자 정의하려면 form_classname 속성(StructBlock 생성자에 대한 키워드 인수로 또는 하위 클래스의 Meta 에서)을 지정하여 기본값인 struct-block 을 재정의할 수 있습니다.

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

    class Meta:
        icon = 'user'
        form_classname = 'person-block struct-block'

그런 다음 insert_global_admin_css 후크를 사용하여 지정된 클래스 이름을 대상으로 하는 이 블록에 대한 사용자 정의 CSS를 제공할 수 있습니다.

참고

Wagtail의 편집기 스타일링에는 struct-block 클래스 및 기타 관련 요소에 대한 일부 내장 스타일링이 있습니다. form_classname 에 대한 값을 지정하면 StructBlock 에 이미 적용된 클래스를 덮어쓰므로 struct-block 도 지정해야 합니다.

HTML 마크업 변경도 필요한 더 광범위한 사용자 정의의 경우 Meta 에서 form_template 속성을 재정의하여 자체 템플릿 경로를 지정할 수 있습니다. 이 템플릿에서 사용할 수 있는 변수는 다음과 같습니다.

childrenStructBlock 을 구성하는 모든 자식 블록에 대한 BoundBlockOrderedDict 입니다.

help_text 이 블록에 대한 도움말 텍스트(지정된 경우).

classname form_classname 으로 전달된 클래스 이름(기본값은 struct-block).

block_definition 이 블록을 정의하는 StructBlock 인스턴스입니다.

prefix 이 블록 인스턴스의 양식 필드에 사용되는 접두사로, 양식 전체에서 고유하도록 보장됩니다.

추가 변수를 추가하려면 블록의 get_form_context 메서드를 재정의할 수 있습니다.

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

    def get_form_context(self, value, prefix='', errors=None):
        context = super().get_form_context(value, prefix=prefix, errors=errors)
        context['suggested_first_names'] = ['John', 'Paul', 'George', 'Ringo']
        return context

    class Meta:
        icon = 'user'
        form_template = 'myapp/block_forms/person.html'

StructBlock에 대한 양식 템플릿은 children 사전의 각 자식 블록에 대한 render_form 의 출력을 블록 이름과 동일한 data-contentpath 속성을 가진 컨테이너 요소 내에 포함해야 합니다. 이 속성은 주석 프레임워크에서 주석을 올바른 필드에 첨부하는 데 사용됩니다. StructBlock의 양식 템플릿은 각 필드에 대한 레이블을 렌더링하는 역할도 하지만, 이(및 기타 모든 HTML 마크업)는 원하는 대로 사용자 정의할 수 있습니다. 아래 템플릿은 기본 StructBlock 양식 렌더링을 복제합니다.

{% load wagtailadmin_tags  %}

<div class="{{ classname }}">
    {% if help_text %}
        <span>
            <div class="help">
                {% icon name="help" classname="default" %}
                {{ help_text }}
            </div>
        </span>
    {% endif %}

    {% for child in children.values %}
        <div class="w-field" data-field data-contentpath="{{ child.block.name }}">
            {% if child.block.label %}
                <label class="w-field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}{% if child.block.required %}<span class="w-required-mark">*</span>{% endif %}</label>
            {% endif %}
            {{ child.render_form }}
        </div>
    {% endfor %}
</div>

StructBlock 양식의 추가 JavaScript

종종 StructBlock 양식에 사용자 정의 JavaScript 동작을 첨부하는 것이 바람직할 수 있습니다. 예를 들어 다음과 같은 블록이 주어지면

class AddressBlock(StructBlock):
    street = CharBlock()
    town = CharBlock()
    state = CharBlock(required=False)
    country = ChoiceBlock(choices=[
        ('us', 'United States'),
        ('ca', 'Canada'),
        ('mx', 'Mexico'),
    ])

미국 이외의 국가를 선택하면 ‘state’ 필드를 비활성화하고 싶을 수 있습니다. 새 블록을 동적으로 추가할 수 있으므로 StreamField 자체의 프런트엔드 논리와 통합하여 새 블록이 초기화될 때 사용자 정의 JavaScript 코드가 실행되도록 해야 합니다.

StreamField는 telepath 라이브러리를 사용하여 StructBlock 과 같은 Python 블록 클래스를 해당 JavaScript 구현에 매핑합니다. 이러한 JavaScript 구현은 다음 클래스로 window.wagtailStreamField.blocks 네임스페이스를 통해 액세스할 수 있습니다.

  • FieldBlockDefinition

  • ListBlockDefinition

  • StaticBlockDefinition

  • StreamBlockDefinition

  • StructBlockDefinition

먼저 AddressBlock 에 대한 telepath 어댑터를 정의하여 기본 StructBlockDefinition 대신 자체 JavaScript 클래스를 사용하도록 합니다. 이는 AddressBlock 정의와 동일한 모듈에서 수행할 수 있습니다.

from wagtail.blocks.struct_block import StructBlockAdapter
from wagtail.telepath import register
from django import forms
from django.utils.functional import cached_property

class AddressBlockAdapter(StructBlockAdapter):
    js_constructor = 'myapp.blocks.AddressBlock'

    @cached_property
    def media(self):
        structblock_media = super().media
        return forms.Media(
            js=structblock_media._js + ['js/address-block.js'],
            css=structblock_media._css
        )

register(AddressBlockAdapter(), AddressBlock)

여기서 'myapp.blocks.AddressBlock' 은 telepath 클라이언트 측 코드에 등록될 JavaScript 클래스의 식별자이고 'js/address-block.js' 는 이를 정의하는 파일입니다(Django에서 인식하는 모든 정적 파일 위치 내의 경로). 이 구현은 StructBlockDefinition을 하위 클래스로 지정하고 render 메서드에 사용자 정의 코드를 추가합니다.

class AddressBlockDefinition extends window.wagtailStreamField.blocks
    .StructBlockDefinition {
    render(placeholder, prefix, initialState, initialError) {
        const block = super.render(
            placeholder,
            prefix,
            initialState,
            initialError,
        );

        const stateField = document.getElementById(prefix + '-state');
        const countryField = document.getElementById(prefix + '-country');
        const updateStateInput = () => {
            if (countryField.value == 'us') {
                stateField.removeAttribute('disabled');
            } else {
                stateField.setAttribute('disabled', true);
            }
        };
        updateStateInput();
        countryField.addEventListener('change', updateStateInput);

        return block;
    }
}
window.telepath.register('myapp.blocks.AddressBlock', AddressBlockDefinition);

StructBlock 값에 대한 추가 메서드 및 속성

템플릿에서 StreamField 콘텐츠를 렌더링할 때 StructBlock 값은 자식 블록의 이름에 해당하는 키가 있는 dict 와 유사한 객체로 표시됩니다. 구체적으로 이러한 값은 wagtail.blocks.StructValue 클래스의 인스턴스입니다.

때로는 이 객체에서 추가 메서드나 속성을 사용할 수 있도록 하는 것이 바람직합니다. 예를 들어 내부 또는 외부 링크를 나타내는 StructBlock이 주어지면

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

채워진 항목에 따라 페이지 URL 또는 외부 URL을 반환하는 url 속성을 사용할 수 있도록 할 수 있습니다. 일반적인 실수는 이 속성을 블록 클래스 자체에 정의하는 것입니다.

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    @property
    def url(self):  # INCORRECT - 작동하지 않음
        return self.external_url or self.page.url

템플릿에서 볼 수 있는 값은 LinkBlock 의 인스턴스가 아니기 때문에 작동하지 않습니다. StructBlock 인스턴스는 블록의 동작에 대한 사양으로만 사용되며 내부 상태에 블록 데이터를 보유하지 않습니다. 이 점에서 Django의 양식 위젯 객체와 유사합니다(주어진 값을 양식 필드로 렌더링하는 메서드를 제공하지만 값 자체를 보유하지는 않음).

대신 사용자 정의 속성 또는 메서드를 구현하는 StructValue 의 하위 클래스를 정의해야 합니다. 이 메서드 내에서 블록의 데이터는 StructValue 가 사전과 유사한 객체이므로 self['page'] 또는 self.get('page') 로 액세스할 수 있습니다.

from wagtail.blocks import StructValue


class LinkStructValue(StructValue):
    def url(self):
        external_url = self.get('external_url')
        page = self.get('page')
        return external_url or page.url

이것이 정의되면 블록의 value_class 옵션을 설정하여 일반 StructValue 대신 이 클래스를 사용하도록 지시합니다.

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    class Meta:
        value_class = LinkStructValue

이제 확장된 값 클래스 메서드를 템플릿에서 사용할 수 있습니다.

{% for block in page.body %}
    {% if block.block_type == 'link' %}
        <a href="{{ link.value.url }}">{{ link.value.text }}</a>
    {% endif %}
{% endfor %}

사용자 정의 블록 유형

사용자 정의 UI를 구현해야 하거나 Wagtail의 내장 블록 유형에서 제공하지 않는 데이터 유형을 처리해야 하는 경우(그리고 기존 필드의 구조로 구축할 수 없는 경우) 자체 사용자 정의 블록 유형을 정의할 수 있습니다. 자세한 내용은 Wagtail의 내장 블록 클래스의 소스 코드를 참조하십시오.

기존 Django 양식 필드를 단순히 래핑하는 블록 유형의 경우 Wagtail은 도우미로 추상 클래스 wagtail.blocks.FieldBlock 을 제공합니다. 하위 클래스는 양식 필드 객체를 반환하는 field 속성을 설정해야 합니다.

class IPAddressBlock(FieldBlock):
    def __init__(self, required=True, help_text=None, **kwargs):
        self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
        super().__init__(**kwargs)

StreamField 편집 인터페이스는 블록을 동적으로 생성해야 하므로 특정 복잡한 위젯 유형에는 클라이언트 측에서 렌더링하고 채우는 방법을 정의하는 추가 JavaScript 코드가 필요합니다. 필드가 django.forms.widgets.Input, django.forms.Textarea, django.forms.Select 또는 django.forms.RadioSelect 에서 상속하는 클래스 중 하나에서 상속하지 않는 위젯 유형을 사용하거나, 양식 요소의 value 속성에 액세스하여 데이터를 읽거나 쓸 수 없는 정도로 클라이언트 측 동작을 사용자 정의한 경우 폼 위젯 클라이언트 측 API에 자세히 설명된 메서드를 구현하는 JavaScript 핸들러 객체를 제공해야 합니다.

마이그레이션 내에서 블록 정의 처리

Django의 모든 모델 필드와 마찬가지로 StreamField에 영향을 미치는 모델 정의에 대한 모든 변경 사항은 해당 필드 정의의 ‘고정된’ 복사본을 포함하는 마이그레이션 파일이 됩니다. StreamField 정의는 일반적인 모델 필드보다 복잡하므로 프로젝트의 정의가 마이그레이션으로 가져올 가능성이 높아집니다. 나중에 해당 정의가 이동되거나 삭제되면 문제가 발생할 수 있습니다.

이를 완화하기 위해 StructBlock, StreamBlock 및 ChoiceBlock은 이러한 블록의 모든 하위 클래스가 StructBlock, StreamBlock 및 ChoiceBlock의 일반 인스턴스로 분해되도록 하는 추가 논리를 구현합니다. 이러한 방식으로 마이그레이션은 사용자 정의 클래스 정의에 대한 참조를 갖지 않습니다. 이는 이러한 블록 유형이 상속에 대한 표준 패턴을 제공하고 해당 패턴을 따르는 모든 하위 클래스에 대한 블록 정의를 재구성하는 방법을 알고 있기 때문에 가능합니다.

FieldBlock 과 같은 다른 블록 클래스를 하위 클래스로 지정하는 경우 해당 클래스 정의를 프로젝트 수명 동안 그대로 유지하거나 Adding a deconstruct() method를 구현하여 블록을 완전히 표현해야 합니다. 마찬가지로 StructBlock, StreamBlock 또는 ChoiceBlock 하위 클래스를 기본 블록 유형의 인스턴스로 더 이상 표현할 수 없는 지점까지 사용자 정의하는 경우(예: 생성자에 추가 인수를 추가하는 경우) 자체 deconstruct 메서드를 제공해야 합니다.