태그 기능¶
Wagtail은 두 가지 Django 모듈을 조합하여 태그 기능을 제공합니다.
django-taggit - 일반적인 용도의 태그 구현을 제공합니다.
django-modelcluster - django-taggit의
TaggableManager를 확장하여 미리보기 및 리비전 처리에 필요한 데이터베이스 쓰기 없이 메모리에서 태그 관계를 관리할 수 있게 합니다.
페이지 모델에 태그 추가하기¶
페이지 모델에 태그 기능을 추가하려면, django-taggit의 Tag 모델과 페이지 모델 간의 다대다 관계를 설정하기 위해 TaggedItemBase 를 상속하는 ‘중간(through)’ 모델을 정의해야 하며, 이 관계를 단일 태그 필드로 표현하기 위해 페이지 모델에 ClusterTaggableManager 접근자를 추가해야 합니다.
다음 예제에서는 BlogPageTag 모델을 통해 BlogPage 에 태그 기능을 설정합니다:
# models.py
from modelcluster.fields import ParentalKey
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey('demo.BlogPage', on_delete=models.CASCADE, related_name='tagged_items')
class BlogPage(Page):
...
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
promote_panels = Page.promote_panels + [
...
FieldPanel('tags'),
]
Wagtail의 관리자 인터페이스는 타입어헤드 태그 자동완성과 친숙한 태그 아이콘을 포함하여 콘텐츠에 태그를 입력하기 위한 멋진 인터페이스를 제공합니다.
이제 뷰와 템플릿에서 다대다 태그 관계를 활용할 수 있습니다. 예를 들어, 블로그의 인덱스 페이지가 태그별로 BlogPage 목록을 필터링하기 위해 ?tag=... 쿼리 매개변수를 받도록 설정할 수 있습니다:
from django.shortcuts import render
class BlogIndexPage(Page):
...
def get_context(self, request):
context = super().get_context(request)
# 블로그 항목 가져오기
blog_entries = BlogPage.objects.child_of(self).live()
# 태그로 필터링
tag = request.GET.get('tag')
if tag:
blog_entries = blog_entries.filter(tags__name=tag)
context['blog_entries'] = blog_entries
return context
여기서 blog_entries.filter(tags__name=tag) 는 BlogPage 의 tags 관계를 따라 일치하는 태그 이름을 가진 페이지만 필터링한 후 렌더링을 위해 템플릿에 전달합니다. 이제 blog_page.html 템플릿을 업데이트하여 페이지와 관련된 태그 목록을 표시하고 필터링된 인덱스 페이지로 다시 연결되는 링크를 추가할 수 있습니다:
{% for tag in page.tags.all %}
<a href="{% pageurl page.blog_index %}?tag={{ tag }}">{{ tag }}</a>
{% endfor %}
page.tags.all 을 반복하면 page 와 관련된 각 태그가 표시되고, 인덱스로 돌아가는 링크는 BlogIndexPage 모델에 추가된 필터 옵션을 활용합니다. Django 쿼리는 tagged_items 관련 이름 필드를 사용하여 태그와 관련된 BlogPage 객체를 가져올 수도 있습니다.
동일한 접근 방식을 스니펫를 통해 관리되는 페이지가 아닌 모델에 태그를 추가하는 데 사용할 수 있습니다. 이 경우, ClusterTaggableManager 와 호환되도록 모델이 modelcluster.models.ClusterableModel 을 상속해야 합니다.
커스텀 태그 모델¶
위의 예제에서는 새로 생성된 태그가 django-taggit의 기본 Tag 모델에 추가되며, 이는 동일한 레시피를 사용하는 다른 모든 모델뿐만 아니라 Wagtail의 이미지 및 문서 모델과도 공유됩니다. 특히 이는 태그 필드의 자동완성 제안이 이전에 다른 모델에 추가된 태그를 포함한다는 것을 의미합니다. 이를 방지하기 위해 TagBase 를 상속하는 커스텀 태그 모델과 ItemBase 를 상속하는 ‘중간(through)’ 모델을 설정하여 해당 페이지 모델에 대한 독립적인 태그 풀을 제공할 수 있습니다.
from django.db import models
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TagBase, ItemBase
class BlogTag(TagBase):
class Meta:
verbose_name = "블로그 태그"
verbose_name_plural = "블로그 태그"
class TaggedBlog(ItemBase):
tag = models.ForeignKey(
BlogTag, related_name="tagged_blogs", on_delete=models.CASCADE
)
content_object = ParentalKey(
to='demo.BlogPage',
on_delete=models.CASCADE,
related_name='tagged_items'
)
class BlogPage(Page):
...
tags = ClusterTaggableManager(through='demo.TaggedBlog', blank=True)
관리자 내에서 태그 필드는 사용 중인 커스텀 태그 모델을 자동으로 인식하고 해당 태그 모델에서 가져온 자동완성 제안을 제공합니다.
자유 태그 비활성화하기¶
기본적으로 태그 필드는 “자유 태그” 방식으로 작동합니다: 편집자는 필드에 원하는 내용을 입력할 수 있으며, 저장할 때 기존 태그로 인식되지 않는 태그 텍스트는 자동으로 생성됩니다. 이 동작을 비활성화하고 편집자가 데이터베이스에 이미 존재하는 태그만 입력할 수 있도록 하려면, 커스텀 태그 모델에 free_tagging = False 옵션을 사용할 수 있습니다:
from taggit.models import TagBase
from wagtail.snippets.models import register_snippet
@register_snippet
class BlogTag(TagBase):
free_tagging = False
class Meta:
verbose_name = "블로그 태그"
verbose_name_plural = "블로그 태그"
여기서는 관리자(및 적절한 권한을 가진 다른 사용자)에게 허용된 태그 세트를 관리하는 인터페이스를 제공하기 위해 BlogTag 를 스니펫으로 등록했습니다. free_tagging = False 옵션이 설정되면 편집자는 더 이상 태그 필드에 임의의 텍스트를 입력할 수 없으며, 대신 자동완성 드롭다운에서 기존 태그를 선택해야 합니다.
스니펫으로 태그 관리하기¶
프로젝트에서 사용되는 모든 태그를 관리하기 위해 Tag 모델을 Wagtail 관리자를 통해 관리할 수 있는 스니펫으로 등록할 수 있습니다. 이를 통해 메인 메뉴 내에 태그를 추가, 편집 또는 삭제할 수 있는 태그 관리 인터페이스를 가질 수 있습니다.
콘텐츠에서 제거된 태그는 Tag 모델에서 삭제되지 않으며 여전히 타입어헤드 태그 자동완성에 표시됩니다. 따라서 태그 인터페이스를 갖는 것은 필요하지 않은 태그를 완전히 제거하는 좋은 방법입니다.
태그 인터페이스를 추가하려면 프로젝트의 앱 중 하나에 있는 wagtail_hooks.py 파일에 다음 코드 블록을 추가하세요:
from wagtail.admin.panels import FieldPanel
from wagtail.snippets.models import register_snippet
from wagtail.snippets.views.snippets import SnippetViewSet
from taggit.models import Tag
class TagsSnippetViewSet(SnippetViewSet):
panels = [FieldPanel("name")] # name 필드만 표시
model = Tag
icon = "tag" # 필요에 따라 변경
add_to_admin_menu = True
menu_label = "태그"
menu_order = 200 # 3번째 위치에 배치 (000이 1번째, 100이 2번째)
list_display = ["name", "slug"]
search_fields = ("name",)
register_snippet(TagsSnippetViewSet)
Tag 모델에는 name 과 slug 필수 필드가 있습니다. 태그를 추가하기로 결정했다면 name 필드 패널만 표시하는 것이 좋습니다. slug 필드는 name 필드가 채워질 때 자동으로 채워지므로 두 필드 모두에 동일한 이름을 입력할 필요가 없기 때문입니다.