파이썬 웹프로그래밍 - 마크다운

채연·2024년 6월 24일

study

목록 보기
9/12
post-thumbnail

스터디 목표

마크다운을 사용해 블로그의 게시물을 쉽게 작성하고, 사이트맵을 통해 블로그를 직관적으로 확인할 수 있도록 기능을 추가한다. 더하여 블로그 게시물용 피드를 만들고 검색 기능을 추가해 장고를 마무리한다.

(1) 마크다운 필터 만들기

  • pip install markdown==3.4.1
  • 마크다운 구문을 지원하는 템플릿 필터를 만든다.

(2) 사이트에 사이트맵 추가하기

  • 웹사이트의 구조를 검색엔진에 알리는 데 사용되는 파일을 사이트맵이라고 한다. 주로 XML 형식으로 작성되며, 크롤러가 웹사이트 콘텐츠를 인덱싱 하는 데 도움이 되므로 검색 엔진 순위에서 사이트를 더 직관적으로 확인이 가능하다.

(3) 블로그 게시물용 피드 만들기

  • 장고에는 사이트 프레임워크를 사용해서 사이트맵을 만드는 것과 유사한 방식으로 RSS 또는 Atom 피드를 동적으로 생성하는 데 사용할 수 있는 신디케이션 피드 프레임워크가 내장되어 있다.
  • 사용자가 피드를 읽고 새로운 콘텐츠의 알림을 받는 데 사용되는 소프트웨어인 피드 수집가를 사용해서 피드 구독이 가능하다.

(4) 블로그에 검색 기능 추가하기

  • 사용자 입력으로 데이터베이스에서 데이터를 검색하는 기능이다.
  • 블로그 리스트 화면에서 제목 또는 내용에 있는 키워드를 검색하여 결과를 가져오는 기능을 구현한다.

  • templatetags/blog_tags.py
from django.utils.safestring import mark_safe

import markdown
@register.filter(name='markdown')
def markdown_format(text):
    return mark_safe(markdown.markdown(text))
  • blog/post/detail.html
{% extends "blog/base.html" %}
{% load blog_tags %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
    <h1>{{ post.title }}</h1>
    <p class = "date">
        Published {{ post.publish }} by {{ post.author }}
    </p>
    {{ post.body|markdown }}
...
  • templates/blog/post/list.html
{% extends "blog/base.html" %}
{% block title %} My blog {% endblock %}
{% load blog_tags %}
{% block content %}
    <h1>My blog</h1>
    {% if tag %}
        <h2>Posts tagged with "{{ tag.name }}"</h2>
    {% endif %}
    <form method="get" class="search-form">
        {% csrf_token %}
        <input type="text" name="q" placeholder="Search" class="search-input">
        <button type="submit" class="search-button">Search</button>
    </form>
    {% if query %}
        {% if posts %}
            <h2>Posts containing "{{ query }}"</h2>
            <h3>Found result</h3>
            {% for post in posts %}
                <h4>
                    <a href="{{ post.get_absolute_url }}">
                        {{ post.title }}
                    </a>
                </h4>
                {{ post.body|markdown|truncatewords:30|linebreaks }}
            {% empty %}
                <p>There are no results for your query.</p>
            {% endfor %}
        {% else %}
            <p>No posts found for your search query.</p>
        {% endif %}
    {% else %}
        {% for post in posts %}
            <h2>
                <a href="{{ post.get_absolute_url }}">
                    {{ post.title }}
                </a>
            </h2>
            <p class="tags">
                Tags:
                {% for tag in post.tags.all %}
                    <a href="{% url 'blog:post_list_by_tag' tag.slug %}">
                        {{ tag.name }}
                    </a>
                    {% if not forloop.last %}, {% endif %}
                {% endfor %}
            </p>
            <p class="date">
                Published {{ post.publish }} by {{ post.author }}
            </p>
            {{ post.body | markdown | truncatewords_html:30 }}
        {% endfor %}
        {% include "pagination.html" with page=posts %}
    {% endif %}
{% endblock %}
  • mysite/settings.py
SITE_ID = 1

INSTALLED_APPS = [
	...,
    "django.contrib.sites",
    "django.contrib.sitemaps",
]
  • blog/sitemaps.py
from django.contrib.sitemaps import Sitemap
from .models import Post

class PostSitemap(Sitemap):
    changefreq = 'weekly'
    priority = 0.9
    def items(self_):
        return Post.published.all()
    def lastmod(self, obj):
        return obj.updated
  • mysite/urls.py
from django.contrib.sitemaps.views import sitemap
from .sitemaps import PostSitemap
from .feeds import LatestPostFeed

sitemaps = {
    'posts':PostSitemap,
}

urlpatterns = [
	...,
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('feed/', LatestPostFeed(), name='post_feed'),
]
  • blog/feeds.py
import markdown
from django.contrib.syndication.views import Feed
from django.template.defaultfilters import truncatewords_html
from django.urls import reverse_lazy
from .models import *
from django.utils.feedgenerator import DefaultFeed

class PostTypeFeed(DefaultFeed):
    content_type = 'application/xml; charset=utf-8'

class LatestPostFeed(Feed) :
    title = 'My blog'
    link = reverse_lazy('blog:post_list')
    description = 'New posts of my blog.'
    feed_type = PostTypeFeed


    def items(self):
        return Post.published.all()[:5]

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return truncatewords_html(markdown.markdown(item.body), 30)

    def item_pubdate(self, item):
        return item.publish
  • blog/base.html
<p>
            <a href="{% url 'blog:post_feed' %}">
                Subscribe to my RSS feed
            </a>
        </p>
        <h3>Latest posts</h3>
  • blog/views.py
from django.db.models import Count, Q

def post_list(request, tag_slug=None):
    post_list = Post.published.all()  # 모든 발행된 게시물 목록 가져옴
    tag = None  # 초기에는 태그가 지정되지 않았으므로 None으로 설정

    if tag_slug:  # 특정 태그가 선택된 경우
        tag = get_object_or_404(Tag, slug=tag_slug)  # 지정된 슬러그를 가진 태그를 가져옴
        post_list = post_list.filter(tags__in=[tag])  # 해당 태그에 속하는 게시물들로 필터링

    search_query = request.GET.get('q')
    if search_query:
        post_list = post_list.filter(
            Q(title__icontains=search_query) | Q(body__icontains=search_query)
        )

    if not tag_slug and not search_query and not post_list.exists():
        messages.info(request, 'No posts found.')

    paginator = Paginator(post_list, 3)  # 한 페이지에 3개의 포스트가 있도록 설정
    page_number = request.GET.get('page', 1)  # page 매개변수가 요청의 GET 파라미터에 없을 경우 기본 값으로 1을 사용
    try:
        posts = paginator.page(page_number)  # 현재 페이지 번호에 해당하는 포스트들을 가져오기
    except PageNotAnInteger:
        posts = paginator.page(1)  # 정수가 아닌 경우 첫 번째 페이지를 다시 염
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)  # 요청한 페이지가 범위를 벗어나면 마지막 페이지 반환

    return render(request, 'blog/post/list.html', {'posts': posts, 'tag': tag, 'query': search_query})
  • mysite/blog/static/blog.css
/* 검색 폼 스타일 */
.search-form{
    margin-bottom: 20px;
}

.search-input{
    width: 200px;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 14px;
}

.search-button{
    padding: 10px 20px;
    background-color: #007bff;
    border: none;
    color: white;
    border-radius: 5px;
    cursor: pointer;
    font-size: 14px;
}

.search-button:hover{
    background-color: #0056b3;
}

실행 결과

Terminal에 python manage.py runserver 명령을 입력하여 출력 결과를 확인한다.



오답노트

모르는 코드 부분을 정확하게 이해하고 정리한다.

(1) python manage.py runserver

  • 문제 :
    python manage.py runserver를 하였을 때 실행 오류가 발생했다.

  • 문제의 원인 :
    데이터베이스의 마이그레이션이 제대로 적용되지 않아 발생하는 오류이다.
    'migrate' 명령어를 실행하지 않으면 장고가 모델을 기반으로 데이터베이스 스키마를 업데이트할 수 없어 필요한 테이블이 생성되지 않는다.

  • 해결 방법 :
    모델을 변경하거나 새로 추가하면 해당 변경 사항을 반영하는 마이그레이션 파일을 생성해야 한다. 또한 생성된 마이그레이션 파일을 데이터베이스에 적용하여 스키마를 업데이트한다.

python manage.py makemigrations
python manage.py migrate

해당 명령어를 작성하여 해결한다.

(2) css

  • 문제 :
    장고 프로젝트에서 css가 적용되지 않는 문제가 발생했다.

  • 문제의 원인 :
    브라우저가 이전에 캐시한 css 파일을 사용하고 있어 새로운 css 파일이 로드되지 않아 생긴 오류이다.

  • 해결 방법 :
    브라우저의 인터넷 사용 기록(캐시)을 지우면 최신 css 파일이 로드되어 해결할 수 있다.

0개의 댓글