사실 일반 목적의 필드를 커스텀 하고 싶을 때는 이후에 사용할 Widget을 커스텀 하는 방법을 사용하면 되지만 Field하고 Panel도 언젠가는 사용할 일이 올 것으로 생각하고 정리해본다.
from django.db.models import TextField
from django.utils.translation import ugettext_lazy as _
from wagtail.admin.edit_handlers import FieldPanel
class MarkdownField(TextField):
def __init__(self, **kwargs):
super(MarkdownField, self).__init__(**kwargs)
class MarkdownPanel(FieldPanel):
def __init__(self, field_name, classname="", widget=None, **kwargs):
super(MarkdownPanel, self).__init__(
field_name,
classname=classname,
widget=widget,
**kwargs
)
if self.classname:
if 'markdown' not in self.classname:
self.classname += "markdown"
else:
self.classname = "markdown"
방법은 간단하다 위에 코드는 사이트에 나온 그대로 가져온 것이다. Django의 TextField를 상속받아서 재정의 해준 것이다. Panel로 마찬가지이다. 다만 panel에서 widget이나 classname을 정의해서 custom 할 수 있다. 이후에 소개할 custom widget을 custom panel에 적용해서 사용할 수도 있겠다.
여기서 몇가지 주의할 점이 있다면 tag는 p태그 대신 div 태그로 했다. p태그로 하니 richtext랑 충돌나는 부분이 있는 것 같아서 제대로 적용되지 않았다. 이후 div태그로 바꿔줘서 적용하니 정상적으로 작동 되었다.
p태그로 했을 때 저장은 되었지만 edit를 할 때
End of block reached without closing inline style elements 오류가 발생하였다. p태그를 적용하게 되면 폰트 적용을 할때 줄바꿈이 일어나므로 오류가 생기는 것으로 파악하고 있다. p태그 이외는 정상적으로 적용이 된다. 태그는 상황에 맞게 바꿔서 사용을 하면 될 것으로 생각한다.
그리고 control 부분에 style을 살펴보면 폰트를 가져올 때, 원래라면 link 태그를 통해 css를 가져오나 여기선 style 내부에 정의를 해줘야 하므로 src로 가져왔다.
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를 적용하려면 위에 같이 적용해야한다고 한다.
이런식으로 적용하면 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파일을 적용 할 수 있다.
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 변수에 저장하고 입력해주었다.
4. 추가 JS, CSS 적용
Code mirror의 기본 코드는 admin 페이지 자체에 wagtail hook을 통해 설정하였다. wagtail_hooks.py를 통해 wagtail admin에 추가 정보를 입력할 수 있다.
StreamField 내부에 또 StreamField를 적용하는 방법에 대해서 기술해보려고 한다.
페이지 하나를 여러가지 용도로 사용하여 용도에 따라 양식이 달라야하고 양식에 다중 양식을 추가하려고 해서 이렇게 작성을 해보았다.
일단 중첩 StreamField를 위해 작성을 했기 때문에 완성된 모델은 아니다. 참고용으로 사용하는 것이 제일 적당하다고 생각한다.
from wagtail.images.blocks import ImageChooserBlock
from wagtail.core.blocks import (CharBlock, StreamBlock, StructBlock,)
from wagtail.core.fields import StreamField
# StudyCardBlock 요소
class QuizBlock(StructBlock):
title = CharBlock(required=True)
option_1 = CharBlock(required=True)
option_2 = CharBlock(required=True)
option_3 = CharBlock(required=True)
option_4 = CharBlock(required=True)
class CodeBlock(StructBlock):
title = title = CharBlock(classname="title", required=False)
content = CharBlock(required=False)
class StudyContentBlock(StreamBlock):
quiz = QuizBlock()
code = CodeBlock()
# 카드 종류 양식
class NoticeCardBlock(StructBlock):
title = CharBlock(classname="title", required=False)
content = CharBlock(required=False)
class StudyCardBlock(StructBlock):
title = CharBlock(classname="title", required=False)
image = ImageChooserBlock(required=False)
body = StudyContentBlock()
# CardBlock
class CardBlock(StreamBlock):
notice_block = NoticeCardBlock()
study_block = StudyCardBlock()
일단 StreamField는 Field 대신 Block을 사용한다. CardBlock이라는 곳에 notice card 양식과 study card 양식을 분리해 놓았다. StreamBlock과 StructBlock의 차이점은 StreamBlock은 안에 정의 된 2개의 모델중 어떤 것을 선택 할지 정의해 놓은 것이다. Struct 블록은 말 그대로 구조 모델을 뜻한다. 즉 Struct 블록에서 정의 된 모델들을 StreamBlock에 정의 해놓으면 선택을 할 수 있다는 것이다. (사람들이 이해가 되지 않을 수도 있을 것 같아 사진으로 남겨놓는다.)
StreamBlock
StructBlock
notice card양식은 추가 필드들이 필요 없으므로 StructBlock으로 작성해 놓았다. -> NoticeCardBlock
study card 양식 같은 경우에는 내부에 code와 quiz를 여러개 작성해야 하므로 StreamBlock으로 정의 해 놓았다. -> StudyCardBlock
다시 quiz와 code의 StructBlock을 선택할 수 있게 StreamBlock을 만들었다. 그것이 바로 StudyContentBlock이다.
그리고 여기에는 code와 quiz의 StructBlock을 정의 해놓는다.
그리고 models.py에 가서 CardBlock만 정의 해 놓으면 된다.
from wagtail.core.fields import StreamField
from wagtail.admin.edit_handlers import StreamFieldPanel
from card.blocks import CardBlock
body = StreamField(
CardBlock(), verbose_name="카드 양식 선택", blank=True
)
content_panels = Page.content_panels + [
StreamFieldPanel('body'),
]
body라는 이름의 변수로 StreamField CardBlock을 가져오고 정의해 놓았다. 여기까지가 모델 정의이고 이제 이것을 html에서 표현 하는 방법에 대해서 작성해보려고 한다.
파일이름은 본인의 Project에 맞게 수정 하면 될 것이다. 일단 block_type을 알아야 하는데 block_type은 StreamBlock에 정의되어 있는 Block 이름이다. 여기서는 중첩 StreamField를 설명하기 위해 study_block을 사용했다.
{% for card_type in page.body %}
{% if card_type.block_type == 'study_block' %}
{% with study_card=card_type.value %}
여기는 page.body변수의 값을 card_type이라는 이름으로 가져오는데 card_type의 block_type이 study_block이면 다음을 실행하라는 뜻이다. 그리고 study_block의 value를 study_card라고 부르려고 with로 정의 해 놓았다. else를 사용하면 notice_block이 나올 것이다.
이제 StreamBlock 내부의 StreamBlock의 값을 가져오는 방법이다. study_card는 위에서 모델에서 정의한 StudyCardBlock을 가르키므로 그곳의 body변수를 content라는 이름으로 불러온다. content에서는 quiz와 code Block이 있으므로 이것을 다시 block_type을 통해 구별해준다. 그리고 이제 content.value를 통해 값 전부를 가져오는데 예를 들자면 with code=content.value면 content.value의 모든 값들을 code라는 이름으로 저장하는 것이다. code에는 모델 변수명과 값들이 전부 들어 있다. 값만 출력하고 싶다면 code.변수명으로 작성해주면 된다.
다음에 다시 한번 읽어보고 설명을 수정해야겠다. 의식의 흐름대로 작성하니 나빼고 이해가 안될 수도 있을 것이라는 생각이 든다.
먼저 User를 정의하기 위해 새로운 User app을 생성 해줘야한다. (물론 기존 app으로도 가능하지만 대신 models.py에 Page 모델이 존재하면 안된다. Wagtail에서 Page 모델은 User 모델을 포함하기 때문이다. 순환 참조 오류가 발생하니 그냥 user app을 새로 생성해주자.)
2. User app 정의
user/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
country = models.CharField(verbose_name='country', max_length=255)
status = models.ForeignKey(MembershipStatus, on_delete=models.SET_NULL, null=True, default=1)
유저 모델을 커스텀 하기 위해서는 AbstractUser를 사용한다.
settings/base.py
AUTH_USER_MODEL = 'users.User'
그리고 base.py에서 우리가 커스텀 하는 유저 모델을 재정의 해준다.
wagtail admin Page의 setting에 있는 user 모델에도 우리가 추가한 필드를 정의해주기 위해서 다음과 같이 forms.py 파일을 만들고 작성한다.
user/forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from wagtail.users.forms import UserEditForm, UserCreationForm
from users.models import MembershipStatus
class CustomUserEditForm(UserEditForm):
country = forms.CharField(required=True, label=_("Country"))
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
class CustomUserCreationForm(UserCreationForm):
country = forms.CharField(required=True, label=_("Country"))
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
forms.py에다가 우리가 추가한 필드의 생성과 수정을 할 때 사용할 form을 정의 해준다.
그리고 users form.html도 수정이 필요하다. templates 폴더를 만들고 wagtailusers폴더을 만들어주고 다시 users폴더도 만들어주자 그리고 나선 create.html과 edit.html이라는 파일을 만들고 아래와 같이 정의해주자.
3. User tempate 정의
templates/wagtailusers/users/create.html
{% extends "wagtailusers/users/create.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% endblock extra_fields %}
templates/wagtailusers/users/edit.html
{% extends "wagtailusers/users/edit.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% endblock extra_fields %}
간단히 정의하자면 country와 status라는 이름의 custom form을 추가하여 사용하겠다는 뜻이다.
Wagtail은 Django base로 만들어진 CMS이므로 user 관리가 Django랑 같다.
바로 allauth를 사용하면 된다. 순서대로 실행해보자
1. All Auth 설치, Setting
$ pip install django-allauth
이후에 settings/base.py를 다음과 같이 추가하고 수정해준다.
# base.py
TEMPLATES = [
{
# ...,
'OPTIONS': {
'context_processors': [
# ...
'django.template.context_processors.request', # <- this is the one you NEED to include
# ...
],
},
},
]
Allauth backend 인증을 위해 추가해준다.
# base.py
# Authentication Backends
AUTHENTICATION_BACKENDS = (
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
'allauth.account.auth_backends.AuthenticationBackend',
)
Authentications_backend는 Django에서 back를 어떻게 사용할지 정보를 적어주는 곳이다.
'django.contrib.auth.backends.ModelBackend'는 All auth와 상관없이 username으로 Django admin에 로그인 하기 위해 사용한다고 한다.
'allauth.account.auth_backends.AuthenticationBackend' 이메일 로그인과 같이 구체적인 로그인 방법을 위해 사용한다고 한다.