Code mirror를 적용해야해서 Wagtail Admin 에서 Code Block Field를 만들어 보기로 했다. 많은 시행착오가 있었지만 결국 해결이 되어 적용방법을 적어보려고 한다. 

https://docs.wagtail.io/en/latest/advanced_topics/customisation/streamfield_blocks.html

 

How to build custom StreamField blocks — Wagtail Documentation 2.15a0 documentation

 

docs.wagtail.io

Wagtail 공식 문서 custom StreamField Block 이다. Wagtail 문서에 찾는 모든 것이 있는데 전부 영어로 되어 있어서 간과하고 지나간 것이 많다. 꼼꼼하게 읽어보니 필요한 내용을 찾을 수 있었다. 

 

문서에서 자바스크립트를 적용한 forms를 만드는 방법에 대해서 나와 있었다.

문서의 코드보단 직접 적용한 코드를 적어보려고 한다. 

 

 

1. Custom Block 정의

Custom block을 하나 만들어서 CodingBlock으로 적용했다. 

card/widgets.html

from wagtail.core.blocks import FieldBlock
from django import forms


class CodingBlock(FieldBlock):
    
    def __init__(self, required=True, help_text=None, **kwargs):
        self.field = forms.CharField(required=False, widget=forms.Textarea()) # attrs={'class':'codingblock'}
        super().__init__(**kwargs)

Wagtail에서는 FieldBlock을 상속받아 나만의 custom block을 만들 수 있는데 Code mirror에서는 Textarea가 필요해서 Textarea로 만들었다. 바로 forms.Textarea()로 적용하려 했더니 되지 않아서 검색해 본 결과 Textarea를 적용하려면 위에 같이 적용해야한다고 한다.

참고로 Custom block을 설정하면 추가 attrs를 적용할 수 있다. 

self.field = forms.CharField(required=False, widget=forms.Textarea(attrs={'class':'codingblock'}))

이런식으로 적용하면 class를 codingblock이라는 textarea를 만들 수가 있다. 하지만 id값은 재정의 되지 않는다. 

 

2. Block 정의

card/models.py

from django import forms
from django.utils.functional import cached_property
from card.widgets import CodingBlock
from django.forms import Media


class CodeBlock(StructBlock):
    code = CodingBlock()


class CodeBlockAdapter(StructBlockAdapter):
    js_constructor = 'card.blocks.CodeBlock'

    @cached_property
    def media(self):
        structblock_media = super().media
        return forms.Media(
            js=structblock_media._js + ['js/mycode.js'],
        )

register(CodeBlockAdapter(), CodeBlock)

StructBlock을 상속받아 CodeBlock을 만들어 준다. 

wagtail에는 StructBlockAdapter라는 것이 있다. 이걸 사용해서 자신이 원하는 블럭의 js파일을 적용 할 수 있다. 

return forms.Media(
            js=structblock_media._js + ['js/mycode.js'],
        )

여기에서 자신이 원하는 js 파일을 넣어주면 된다. 

 

3. JavaScript 정의

static/js/mycode.js

class CodeBlockDefinition extends window.wagtailStreamField.blocks.StructBlockDefinition {
    render(placeholder, prefix, initialState, initialError) {
        const block = super.render(placeholder, prefix, initialState, initialError);
        const textareaid = prefix + '-code';

        var editor = CodeMirror.fromTextArea(document.getElementById(textareaid), {
            mode: "python",
            theme: "dracula",
            lineNumbers: true,
        });
        editor.setSize("100%","100%"); //사이즈 설정으로 codemirror 크기를 조절할 수 있다.
        return block;
    }
}
window.telepath.register('card.blocks.CodeBlock', CodeBlockDefinition);

다만, 기존처럼 js코드만 쓰면 되는 것이 아니고 telepath를 이용하여 textarea의 정보를 가져와야 한다. 여기서 prefix라는 것은 block의 행순서를 말한다. block이 여러개가 추가 될 수록 prefix 값이 달라지고 이를 통해서 구별하게 된다. 그리고 이를 id 값으로 사용한다. 정리하자면

body-0-value-body-0-value

body-0-value-body-1-value

body-0-value-body-2-value

이런식으로 저장되어진다. 

그리고 뒤에 필드명인 code가 붙어서

body-0-value-body-0-value-code 이런형태로 id값이 설정되게 된다. 나는 Code Mirror를 적용하였다.

Code Mirror는 id 값을 입력 해줘야하므로 prefix와 field명인 code를 합한 각 textarea의 아이디명을 textareaid 변수에 저장하고 입력해주었다.

적용된 code mirror

4. 추가 JS, CSS 적용

Code mirror의 기본 코드는 admin 페이지 자체에 wagtail hook을 통해 설정하였다. wagtail_hooks.py를 통해 wagtail admin에 추가 정보를 입력할 수 있다. 

card/wagtail_hooks.py

@hooks.register('insert_editor_css')
def editor_css():
    css_files = [
        'codemirror/lib/codemirror.css',
        'codemirror/theme/dracula.css',
    ]
    css_includes = format_html_join('\n', '<link rel="stylesheet" href="{0}"></link>',
        ((static(filename),) for filename in css_files)
    )
    return css_includes


@hooks.register('insert_editor_js')
def editor_js():
    js_files = [
        'codemirror/lib/codemirror.js',
        'codemirror/mode/xml/xml.js',
        'codemirror/mode/python/python.js',
    ]
    js_includes = format_html_join('\n', '<script src="{0}"></script>',
        ((static(filename),) for filename in js_files)
    )
    return js_includes

wagtail hook에 대해서는 다음에 문서로 전체 정리해보려고 한다. 당장은 필요한 부분만 기술하였다. 

(wagtail_hooks.py파일은 wagtail이 실행될 때 파일을 찾아서 자동으로 실행시키게 된다.) 

 

insert_global_admin_js와 insert_editor_js가 있는데 둘의 차이는 script 위치를 어디에다가 적용할 것이냐의 차이가 있다. 여러개의 css파일이나 js파일을 적용하고 싶다면 위와 같이 정의해 놓으면 된다. 

 

https://docs.wagtail.io/en/stable/reference/hooks.html#admin-hooks

추가적인 정보가 필요하면 윗 문서를 참고하자.

추가)Custom editing interfaces for StructBlock

Page를 생성할 때 StruckBlock의 layout 설정이나 js,css를 적용하려고 할 때 참고하자.

myapp/blocks.py

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'

공식문서 그대로 코드를 가져왔다. class Meta에 form_template의 수정을 통해 원하는 레이아웃이나 css를 적용할 수 있다.

 

templates/block_forms/person.html

{% load wagtailadmin_tags  %}

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

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

위가 wagtail의 admin field의 기본 구조이다. 

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

child가 우리가 작성한 field들의 정보가 들어 있는 곳이다. 

child.block.name이 필드의 변수 명이므로 if 문을 사용하여 필드를 구별해서 원하는대로 custom을 하면 된다. 

ex) {% if child.block.name == "변수이름" %} . . . {% endif %}

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

Wagtail Custom Field, Panel  (0) 2021.09.29
Wagtail hooks로 RichTextField에 구글 폰트 적용  (0) 2021.09.19
Wagtail 중첩 StreamField  (0) 2021.09.01
Wagtail Custom User Field  (0) 2021.09.01
Wagtail All Auth(Google Login)  (0) 2021.08.25

+ Recent posts