django

[Django] 게시글에 태그 붙이기

support_u 2023. 5. 3. 16:15

tag를 도와줄 오픈소스를 받아온다.

- 하나의 필드에 여러 개의 태그를 추가하거나 삭제

- 태그 이름을 통해 모델 인스턴스를 검색 & 그룹화하고 집계

- 자동 완성 기능과 연동(django-taggit-autosuggest 패키지를 함께 사용)

 

Getting Started — django-taggit 1.3.0 documentation

Add "taggit" to your project’s INSTALLED_APPS setting. Run ./manage.py migrate. Note If you want django-taggit to be CASE-INSENSITIVE when looking up existing tags, you’ll have to set TAGGIT_CASE_INSENSITIVE (in settings.py or wherever you have your Dj

django-taggit.readthedocs.io

1. 오픈소스 다운

pip install django-taggit

pip install django-taggit-templatetags2

 

2. settings.py

INSTALLED_APPS = [
	...
	'taggit.apps.TaggitAppConfig',
    'taggit_templatetags2',
    ...
]



TAGGIT_CASE_INSENSITIVE = True
TAGGIT_LIMIT = 50

- taggit 설정

# 대소문자 처리
TAGGIT_CASE_INSENSITIVE = True

# 태그 클라우드에서 표시할 최대 태그 수
TAGGIT_LIMIT = 50

# 태그 최대 길이(기본 50)
TAGGIT_MAX_TAG_LENGT = 30

# 빈 태그 생성을 막는다
TAGGIT_REMOVE_EMPTY = True

# 문자열에서 태그 추출 구분자(기본',')
TAGGIT_TAGS_FROM_STRING = ''

# 모델에서 태그 추출 구분자(기본',')
TAGGIT_TAGS_FROM_MODEL = ''

# 중복 태그를 허용하는가 (기본 False)
TAGGIT_ALLOW_DUPLICATES = False

 

3. models.py에 적용

from taggit.managers import TaggableManager
from django.urls import reverse

class Article(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    like_users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='like_articles')
    bookmark_user = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='bookmark_articles')
    views = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='view_articles')
    # 태그를 넣어준다
    tags = TaggableManager(blank=True)
    
    # tag를 쓴 detail에 연결이 편하기 위해 위해사용
	def get_absolute_url(self):
        return reverse('articles:detail', args=[int(self.id)])

 

4. admin.py

admin.site.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('title', 'content', 'tag_list',)
    list_filter = ('tag_list',)
    # 검색 박스 표시, title, content 칼럼에서 검색
    search_fields = ('title', 'content')
    # title 필드를 사용해 미리 채워지도록
    prepopulated_fields = {'slug':('title',)}
	
    # Article 레코드 리스트를 가져오는 메소드 오버라이딩
    # N:N 관계에서 쿼리 횟수를 줄여 성능 높일때 : prefetch_related
    def get_queryset(self, request):
        return super().get_queryset(request).prefetch_related('tags')
    
    def tag_list(self, obj):
        return ', '.join(o.name for o in obj.tags.all())

 

5. migrate

 

6. urls.py

    #  태그 목록을 볼 수 있는 url 
    # TemplateView를 상속받아 정의
    path('tag/', views.TagCloudTV.as_view(), name='tag_cloud'),
    # 태그가 달린 게시글들의 목록
    # ListView를 상속받아 정의
    path('tag/<str:tag>/', views.TaggedObjectLV.as_view(), name='tagged_object_list')

 

7. views.py

create 때 tag를 받아 저장

@login_required
def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        images = request.FILES.getlist('image')
        # 태그를 , 기준으로 분리
        tags = request.POST.get('tags').split(',')
        if form.is_valid():
            article = form.save(commit=False)
            article.user = request.user
            article.save()
            # tags를 하나씩 article.tags에 넣어준다
            for tag in tags:
                article.tags.add(tag.strip())
            for i in images:
                ArticleImage.objects.create(article=article, image=i)
            return redirect('articles:index')       
        else:
            print(form.errors)
    else:
        form = ArticleForm()
        imageform = ArticleImageForm()
    context = {
        'form': form,
        'imageform' : imageform

    }
    return render(request, 'articles/create.html', context)

 

detail에 태그가 나오도록 출력

def detail(request, article_pk):
    article = Article.objects.get(pk=article_pk)
    # view
    if request.user not in article.views.all():
        article.views.add(request.user)

    comment_form = ArticleCommentForm()
    comments = article.articlecomment_set.all()

    tags = article.tags.all()


    context = {
        'article': article,
        'comment_form' : comment_form,
        'comments' : comments,
        'tags' : tags
    }
    return render(request, 'articles/detail.html', context)

 

태그목록과 태그를 쓴 게시글을 보여줄 html

from django.views.generic import ListView, TemplateView

class TagCloudTV(TemplateView):
    template_name = 'taggit/taggit_cloud.html'

class TaggedObjectLV(ListView):
    template_name = 'taggit/taggit_article_list.html'
    model = Article

    def get_queryset(self):
        return Article.objects.filter(tags__name=self.kwargs.get('tag'))
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['tagname'] = self.kwargs['tag']
        return context

 

8. templates

forms.py에서 create때 tag를 받을 수 있도록 저장

class ArticleForm(forms.ModelForm):
    tags = forms.CharField(
        widget=forms.TextInput(
            attrs={
                'class': 'form-control',
                'placeholder' : '태그 사이에 쉼표를 넣어주세요.'
            }
        )
    )

    class Meta:
        model = Article
        fields = ('tags')

 

detail.html

    <div>
        <b>TAGS</b><br>
        # taggit_templatetag2_tags 모듈을 가져온다
        {% load taggit_templatetags2_tags %}
        # object(artice)의 태그를 가져온다
       {% get_tags_for_object article as "tags" %} <!-- ** -->
        {% for tag in tags %}
        <a href="{% url 'articles:tagged_object_list' tag.name %}">{{tag.name}}</a>
        {% endfor %} 

    </div>

 

taggit_article_list.html

    <h1>As for tag - {{ tagname }}</h1>
    
    # object_list 객체는 TaggedObjectLV 클래스형 뷰에서 넘겨주는 컨텍스트 변수로서 Article 리스트가 담겨있음
    {% for article in object_list %} 
    	# 각 디테일 페이지로 연결
        <h2><a href='{{ article.get_absolute_url }}'>{{ article.title }}</a></h2>
        {{tag.num_times}}
    {% endfor %}

 

taggit_cloud.html

{% extends "base.html" %}

{% block style %}
<style>
    .tag-cloud{
        width: 40%;
        margin-left: 30px;
        text-align: center;
        padding: 5px;
        border: 1px solid orange;
        background-color: #ffc;
    }
    .tag-1 {font-size: 12px}
    .tag-2 {font-size: 14px}
    .tag-3 {font-size: 16px}
    .tag-4 {font-size: 18px}
    .tag-5 {font-size: 20px}
    .tag-6 {font-size: 24px}
</style>
{% endblock style %}


{% block content %}
<article>
    <h1>Article Tag Cloud</h1>

    <div class="tag-cloud">
        {% load taggit_templatetags2_tags %}
        # 모든 태그 추출해서 tags변수에 할당
        {% get_tagcloud as tags %} 
        {% for tag in tags %}
        <span class="tag-{{tag.weight|floatformat:0}}"> <!-- style 관련-->
            <a href="{% url 'articles:tagged_object_list' tag.name %}"> {{tag.name}}({{tag.num_times}})</a>
        </span>
        {% endfor %}
    </div>
</article>
{% endblock content %}

 

 

---

Error

- NOT NULL constraint failed:

위와 같은 오류가 발생하였다. 이 경우 제약조건때문에 null 값을 넣을 수 없는건데 model에서 아무리 제약을 풀어줘도 안된다면 migrations와 DB를 깔끔하게 날리고 진행하자. 나같은 경우 그 전에 있던 데이터때문에 안됐으며 지우니 깔끔하게 진행되었다.

 

- 태그를 삭제한 경우

태그의 글자는 저장되어있지만, 사용수가 없어지거나 태그가 없는 게시물에서 오류가 남

태그가 빈 문자열이라도 처리할 수 있도록 tag.name을 조건을 붙여줌

{% load taggit_templatetags2_tags %} 
{% get_tags_for_object article as "tags" %}
{% if tags %}
  {% for tag in tags %}
    {% if tag.name %}
    <a href="{% url 'articles:tagged_object_list' tag.name %}">{{tag.name}}</a>
    {% endif %}
  {% endfor %}
{% endif %}

클라우드에서 0인 경우는 보지 않으려고 tag.num_times도 적용

{% load taggit_templatetags2_tags %}
{% get_tagcloud as tags %} <!--모든 태그 추출해서 tags변수에 할당-->
{% for tag in tags %}
<span class="tag-{{tag.weight|floatformat:0}}"> <!-- style 관련-->
    {% if tag.name and tag.num_times %}
        <a href="{% url 'articles:tagged_object_list' tag.name %}">{{ tag.name }}({{ tag.num_times }})</a>
    {% endif %}
</span>
{% endfor %}