Django 개발 (4)

RXDRYD·2021년 8월 21일
0

Head_image 연결 오류

이미지가 없는 상태로 post 를 등록한 경우

ValueError at /blog/
The 'head_image' attribute has no file associated with it.



블로그포스트에 이미지를 보이는 부분인 p.head_image.url 에 분기처리가 되어있지 않아 발생하는 오류

post_list.html

<div class="card mb-4">
  {% if p.head_image %}
      <a href="#!"><img class="card-img-top" src="{{ p.head_image.url }}" alt="..." /></a>
  {% else %}
      <a href="#!"><img class="card-img-top" src="https://picsum.photos/id/1000/750/300" alt="..." /></a>
  {% endif %}

이미지가 있을 경우에는 해당 이미지를, 없을 경우에는 미리 지정된 이미지를 보이기

Lorem ipsum 처럼 이미지 관련 사이트도 있음 -> Lorem picsum ! https://picsum.photos/images 에서 이미지 골라서 추가
<a href="#!"><img class="card-img-top" src="https://picsum.photos/id/1000/750/300" alt="..." />


이미지를 넣지않은 상태로 post 등록을 해도 오류가 발생하지 않는다.

포스트 목록에서 글 내용 축약


너무 길다... 세상 길게 목록인데 내용이 다 나와버림. 축약해서 보여주기

truncatewords ,truncatechars 가 있는데 words 는 단어 단위, chars 는 문자단위로 던지므로 truncatewords 를 이용해 50글자 만 보이게 하기

post_list.html

<div class="card-body">
  <div class="small text-muted">{{ p.created }} by {{ p.author }}</div>
  <h2 class="card-title">{{ p.title }}</h2>
  <p class="card-text">{{ p.content | truncatewords:50 }}</p>
  <a class="btn btn-primary" href="#!">Read more →</a>
</div>

Detailview

http://127.0.0.1:8000/ 에 대한 url 은 따로 지정하지 않았기 때문에 아래 처럼 오류가 발생한다.


블로그 목록에서 Read more 를 선택했을 때 , 지금은 아무 반응이 일어나지않는데 해당 글에 대한 상세한 내용이 보이게 detailview 를 만들어보자.

blog > urls.py

path('<int:pk>/', views.post_detail) pattern 추가
url blog 뒤에 숫자로 표현하기 위함 (ex. /blog/1/)

from django.urls import path, include
from . import views

urlpatterns = [
    path('<int:pk>/', views.post_detail),
    path('', views.PostList.as_view()),
]

blog > views.py

post_detail 에 대해 작성

class PostList(ListView):
    model = Post

    def get_queryset(self):
        return Post.objects.order_by('-created')

def post_detail(request, pk):
    blog_post = Post.objects.get(pk=pk)

    return render(
        request,
        'blog/post_detail.html',
        {
            'blog_post': blog_post,
        }
    )

post_detail.html 파일 생성

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ blog_post.title }}</title>
</head>
<body>
    <h1>{{ blog_post.title }}</h1>
    <div>
        {{ blog_post.content }}
    </div>
</body>
</html>

조금 더 간단하게 구현해보자

좀전에 구현했던 def post_detail을 주석처리, post_detail.html삭제 후

blog > views.py

from DetailView 를 추가해주고 , Class 선언만 하면 끝!

from django.views.generic import ListView, DetailView

class PostDetail(DetailView):
    model = Post

# def post_detail(request, pk):
#     blog_post = Post.objects.get(pk=pk)
#
#     return render(
#         request,
#         'blog/post_detail.html',
#         {
#             'blog_post': blog_post,
#         }
#     )

blog > urls.py

views.post_detail 대신에 views.PostDetail.as_view()) 로 작성

urlpatterns = [
    path('<int:pk>/', views.PostDetail.as_view()),
    path('', views.PostList.as_view()),
]


TemplateDoesNotExist at /blog/1/
blog/post_detail.html
로 에러 발생하는데 . class 명 뒤에 _detail.html 파일이 필요하다고 알아서 알려준다 > post_detail.html 파일 생성

post_detail.html

view 에서 post_detail을 작성할때, 따로 템플릿 이름을 지정하지 않았으므로 그냥 object 를 default로 받아옴

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ object.title }}</title>
</head>
<body>
    <h1>{{ object.title }}</h1>
    <div>
        {{ object.content }}
    </div>

</body>
</html>

TDD (Test Driven Development)

구현 > 브라우저로 직접확인 > 성공 > 개선점찾기
page가 많아지고 구조가 복잡해진다면 나중에는 불가능해질 것

TDD 개발 싸이클

  1. Test code 먼저작성
    • 만들고 싶은 기능에 대한 test 먼저 작성
  2. 기능 구현
    • Test Code 를 만족시키도록 기능 구현
  3. Refactoring
    • 성능향상, 재사용성, 가독성 개선

지금까지 post관련 작업해온 방식은 Django 기능을 살펴보기위해 기능 구현을 우선으로 했기 때문에, Test Code를 작성하는것을 해보자

Test Code 작성

python manage.py test 로, 이 프로젝트 전체에 대한 테스트를 수행함

 ᐅ python manage.py test
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

ᐅ pip install Beautifulsoup4 설치
ᐅ pip list 로 확인
Beautifulsoup 을 통해 브라우저의 태그값을 추출해올 수 있다.

blog > tests.py

from django.test import TestCase, Client
from bs4 import BeautifulSoup

class TestView(TestCase):
    # Client 의 역할을 제공해줌
    def setUp(self):
        self.client = Client()

    # test_ 로 시작하는게 국룰. 왠만하면 변경하지 말기
    def test_post_list(self):
        response = self.client.get('/blog/')
        self.assertEqual(response.status_code, 200)

        # title 태그의 값을 잘 읽어오는지 테스트
        soup = BeautifulSoup(response.content, 'html.parser')
        title = soup.title
        print(title.text)
        self.assertEqual(title.text, 'Blog')

        # navigation bar가 잘 동작하는지 Blog, About me가 있는지 확인
        # post_list.html의 navigation 영역에 id="navbar" 추가
        navbar = soup.find('div', id='navbar')
        self.assertIn('Blog', navbar.text)
        self.assertIn('About me', navbar.text)

터미널 test 결과 (OK)

ᐅ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Blog  <-- 얘가 print(title.text) 부분 
.
----------------------------------------------------------------------
Ran 1 test in 0.039s

OK
Destroying test database for alias 'default'...

블로그에 게시글이 없는 경우 표시

만들어놓은 포스트 글 전체 삭제

TDD를 기반으로 코드 작성

DB 에 쿼리문 예시
Post.objects.all() : 다 가져와
Post.objects.get(pk=pk) : pk 에 해당하는것만 가져와
Post.objects.count() : 갯수 세와

blog > tests.py : Test Code 먼저 구현

from .models import Post
...
...
	# navbar 
    	...
	# 게시글이 없는경우 = object 의 갯수가 하나도 없는경우
	self.assertEqual(Post.objects.count(), 0)
	self.assertIn('아직 게시물이 없습니다', soup.body.text)

터미널 테스트 결과 : not found 로 나오는게 당연함

ᐅ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Blog
F
======================================================================
FAIL: test_post_list (blog.tests.TestView)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/projects/mysite/blog/tests.py", line 29, in test_post_list
    self.assertIn('아직 게시물이 없습니다', soup.body.text)
AssertionError: '아직 게시물이 없습니다' not found in '\n\n\nLab.

post_list.html: '아직 게시물이 없습니다' 삽입

<h1 class="my-4">Blog</h1>
     <h3>아직 게시물이 없습니다</h3>   
     {% for p in object_list %}
     <!-- Featured blog post-->
     ...

터미널 테스트 결과 ok 로 바뀜

ᐅ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Blog
.
----------------------------------------------------------------------
Ran 1 test in 0.030s

OK
Destroying test database for alias 'default'...

tests.py

# 게시글이 없는경우 = object 의 갯수가 하나도 없는경우
self.assertEqual(Post.objects.count(), 0)
self.assertIn('아직 게시물이 없습니다', soup.body.text)

# 게시글이 있는경우 = 게시글 수가 0보다 큰 경우
self.assertGreater(Post.objects.count(), 0)

그리고 blog에 포스팅을 하나 추가하자
Django 페이지 화면에는 포스팅이 하나 추가되었음에도 불구하고...

터미널 테스트 결과 : fail

ᐅ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Blog
F
======================================================================
FAIL: test_post_list (blog.tests.TestView)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/projects/mysite/blog/tests.py", line 32, in test_post_list
    self.assertGreater(Post.objects.count(), 0)
AssertionError: 0 not greater than 0

tests.py 에 작성한걸로 테스트를 수행할때는 실제 DB에 무엇이 들어있던 신경쓰지 않고 수행하므로 만약 포스트글을 추가해서 count가 0을 초과하더라도 0을 초과했는지 여부를 확인하는 테스트에서는 fail이 발생함

-> 테스트는 DB에 아무것도 저장되어있지 않은 (=초기의) 상태로 수행한다고 이해하자

포스트가 하나는 있어야 하는 상황 구현

DB에 아무것도 없는 상태로 테스트하므로 tests.py 내에 임의의 user와 post를 생성해주기

self.author_000 = User.objects.create(username='smith', password='nopassword')
post_000 = Post.objects.create(
            title = 'The first post',
            content = 'Hello world',
            created = timezone.now(),
            author = self.author_000,
        )

tests.py

from django.test import TestCase, Client
from bs4 import BeautifulSoup
from .models import Post
from django.utils import timezone
from django.contrib.auth.models import User

class TestView(TestCase):
    # Client 의 역할을 제공해줌
    def setUp(self):
        self.client = Client()
        self.author_000 = User.objects.create(username='smith', password='nopassword')

    # test_ 로 시작하는게 국룰. 왠만하면 변경하지 말기
    def test_post_list(self):
        response = self.client.get('/blog/')
        self.assertEqual(response.status_code, 200)

        # title 태그의 값을 잘 읽어오는지 테스트
        soup = BeautifulSoup(response.content, 'html.parser')
        title = soup.title
        print(title.text)
        self.assertEqual(title.text, 'Blog')

        # navigation bar가 잘 동작하는지 Blog, About me가 있는지 확인
        # post_list.html의 navigation 영역에 id="navbar" 추가
        navbar = soup.find('div', id='navbar')
        self.assertIn('Blog', navbar.text)
        self.assertIn('About me', navbar.text)

        # 게시글이 없는경우 = object 의 갯수가 하나도 없는경우
        self.assertEqual(Post.objects.count(), 0)
        self.assertIn('아직 게시물이 없습니다', soup.body.text)

        post_000 = Post.objects.create(
            title = 'The first post',
            content = 'Hello world',
            created = timezone.now(),
            author = self.author_000,
        )

        # 게시글이 있는경우 = 게시글 수가 0보다 큰 경우
        self.assertGreater(Post.objects.count(), 0)
        
        # refresh를 했을때 게시물이 없는 상태로 가면 안되므로...
        response = self.client.get('/blog/')
        self.assertEqual(response.status_code, 200)
        soup = BeautifulSoup(response.content, 'html.parser')
        body = soup.body
        # 게시글이 있는 상태인데, 아직 게시물이 없다고 하면 안되므로 NotIn 사용
        self.assertNotIn('아직 게시물이 없습니다', body.text)
        # 있는 게시글 post_000 의 타이틀을 내놓으면 됨
        self.assertIn(post_000.title, body.text)

post_list.html : 게시글 유무 if 문 작성

{% if object_list.exists %}
    {% for p in object_list %}
        <!-- Featured blog post-->
        <div class="card mb-4">
            {% if p.head_image %}
                <a href="#!"><img class="card-img-top" src="{{ p.head_image.url }}" alt="..." /></a>
            {% else %}
                <a href="#!"><img class="card-img-top" src="https://picsum.photos/id/1000/750/300" alt="..." /></a>
            {% endif %}

            <div class="card-body">
                <div class="small text-muted">{{ p.created }} by {{ p.author }}</div>
                <h2 class="card-title">{{ p.title }}</h2>
                <p class="card-text">{{ p.content | truncatewords:50 }}</p>
                <a class="btn btn-primary" href="#!">Read more →</a>
            </div>
        </div>
    {% endfor %}
{% else %}
    <h3>아직 게시물이 없습니다</h3>
{% endif %}

터미널 테스트 결과 : OK

ᐅ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Blog
.
----------------------------------------------------------------------
Ran 1 test in 0.093s

OK
Destroying test database for alias 'default'...

pk url 화면의 Nav bar 테스트하기

tests.py :

def test_post_detail 을 새로 만들면 또 초기상태이므로 포스트를 생성하는 부분을 함수로 별도로 만들자 create_post

from django.contrib.auth.models import User

def create_post(title, content, author):
    blog_post = Post.objects.create(
        title=title,
        content=content,
        created=timezone.now(),
        author=author
    )
    return blog_post

	...
    	...
        
    def test_post_detail(self):
        post_000 = create_post(
            title='The first Post',
            content='Hello world. We are the world',
            author=self.author_000,
        )

        self.assertGreater(Post.objects.count(), 0)

        # getAbsolute URL
        self.assertEqual(post_000.get_absolute_url(), '/blog/{}/'.format(post_000.pk))

models.py : get_absolute_url 추가

    def __str__(self):
        return '{} :: {}'.format(self.title, self.author)

    def get_absolute_url(self):
        return '/blog/{}/'.format(self.pk)

장고화면에 View on site 라고 새로 생긴다. 신기?
장고에서 제공하는 기능으로 이 post 에 대한 view는 이거야~ 하고 알려줌

터미널 테스트 결과 : OK

tests.py :

# getAbsolute URL
post_000_url = post_000.get_absolute_url()
self.assertEqual(post_000_url, '/blog/{}/'.format(post_000.pk))

# 페이지 열어봐야지
response = self.client.get(post_000_url)
self.assertEqual(response.status_code, 200)

# {} - Blog  의 형태로 보여주고 싶다 
soup = BeautifulSoup(response.content, 'html.parser')
title = soup.title
self.assertEqual(title.text, '{} - Blog'.format(post_000.title))

터미널 테스트 결과 : fail

self.assertEqual(title.text, '{} - Blog'.format(post_000.title))
AssertionError: 'The first Post' != 'The first Post - Blog'
- The first Post
+ The first Post - Blog
?               +++++++


----------------------------------------------------------------------
Ran 2 tests in 0.108s

FAILED (failures=1)

post_detail.html : title끝에 - Blog 추가

<title>{{ object.title }} - Blog</title>

tests.py : navbar 체크하는 함수 추가

self.check_navbar(soup) 로 호출

class TestView(TestCase):
    # Client 의 역할을 제공해줌
    def setUp(self):
        self.client = Client()
        self.author_000 = User.objects.create(username='smith', password='nopassword')

    # Navigation Bar를 체크하는 함수
    def check_navbar(self, soup):
        navbar = soup.find('div', id='navbar')
        self.assertIn('Blog', navbar.text)
        self.assertIn('About me', navbar.text)
     ...
     ...
     ...
   	# {} - Blog  의 형태로 보여주고 싶다
        soup = BeautifulSoup(response.content, 'html.parser')
        title = soup.title
        self.assertEqual(title.text, '{} - Blog'.format(post_000.title))
        self.check_navbar(soup)

터미널 테스트 결과 : Fail -> nav bar 가 없는데 .text를 참조하려고 하니 에러가 발생함

ᐅ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
EBlog
.
======================================================================
ERROR: test_post_detail (blog.tests.TestView)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/projects/mysite/blog/tests.py", line 93, in test_post_detail
    self.check_navbar(soup)
  File "/projects/mysite/blog/tests.py", line 26, in check_navbar
    self.assertIn('Blog', navbar.text)
AttributeError: 'NoneType' object has no attribute 'text'

----------------------------------------------------------------------
Ran 2 tests in 0.103s

FAILED (errors=1)
Destroying test database for alias 'default'...

post_detail.html : navbar 추가

post_list.html 의 navbar 부분을 복붙해오기

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ object.title }} - Blog</title>
</head>
<body>
<div class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary" id="navbar">
    <div class="container">
        <a href="../" class="navbar-brand">Lab. Rxdryd</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarResponsive">
            <ul class="navbar-nav">
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" id="themes">Themes <span class="caret"></span></a>
                    <div class="dropdown-menu" aria-labelledby="themes">
                        <a class="dropdown-item" href="../default/">Default</a>
                        <div class="dropdown-divider"></div>
                        <a class="dropdown-item" href="../cerulean/">Cerulean</a>
                        <a class="dropdown-item" href="../cosmo/">Cosmo</a>
                        <a class="dropdown-item" href="../cyborg/">Cyborg</a>
                        <a class="dropdown-item" href="../darkly/">Darkly</a>
                        <a class="dropdown-item" href="../flatly/">Flatly</a>
                        <a class="dropdown-item" href="../journal/">Journal</a>
                        <a class="dropdown-item" href="../litera/">Litera</a>
                        <a class="dropdown-item" href="../lumen/">Lumen</a>
                        <a class="dropdown-item" href="../lux/">Lux</a>
                        <a class="dropdown-item" href="../materia/">Materia</a>
                        <a class="dropdown-item" href="../minty/">Minty</a>
                        <a class="dropdown-item" href="../morph/">Morph</a>
                        <a class="dropdown-item" href="../pulse/">Pulse</a>
                        <a class="dropdown-item" href="../quartz/">Quartz</a>
                        <a class="dropdown-item" href="../sandstone/">Sandstone</a>
                        <a class="dropdown-item" href="../simplex/">Simplex</a>
                        <a class="dropdown-item" href="../sketchy/">Sketchy</a>
                        <a class="dropdown-item" href="../slate/">Slate</a>
                        <a class="dropdown-item" href="../solar/">Solar</a>
                        <a class="dropdown-item" href="../spacelab/">Spacelab</a>
                        <a class="dropdown-item" href="../superhero/">Superhero</a>
                        <a class="dropdown-item" href="../united/">United</a>
                        <a class="dropdown-item" href="../vapor/">Vapor</a>
                        <a class="dropdown-item" href="../yeti/">Yeti</a>
                        <a class="dropdown-item" href="../zephyr/">Zephyr</a>
                    </div>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/blog/">Blog</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/about_me/">About me</a>
                </li>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" id="download">Morph <span class="caret"></span></a>
                    <div class="dropdown-menu" aria-labelledby="download">
                        <a class="dropdown-item" rel="noopener" target="_blank" href="https://jsfiddle.net/bootswatch/f4torgy7/">Open in JSFiddle</a>
                        <div class="dropdown-divider"></div>
                        <a class="dropdown-item" href="../5/morph/bootstrap.min.css" download>bootstrap.min.css</a>
                        <a class="dropdown-item" href="../5/morph/bootstrap.css" download>bootstrap.css</a>
                        <div class="dropdown-divider"></div>
                        <a class="dropdown-item" href="../5/morph/_variables.scss" download>_variables.scss</a>
                        <a class="dropdown-item" href="../5/morph/_bootswatch.scss" download>_bootswatch.scss</a>
                    </div>
                </li>
            </ul>
            {#            <ul class="navbar-nav ms-md-auto">#}
            {#                <li class="nav-item">#}
            {#                    <a target="_blank" rel="noopener" class="nav-link" href="https://github.com/thomaspark/bootswatch/"><i class="fa fa-github"></i> GitHub</a>#}
            {#                </li>#}
            {#                <li class="nav-item">#}
            {#                    <a target="_blank" rel="noopener" class="nav-link" href="https://twitter.com/bootswatch"><i class="fa fa-twitter"></i> Twitter</a>#}
            {#                </li>#}
            {#            </ul>#}
        </div>
    </div>
</div>
<h1>{{ object.title }}</h1>
<div>
    {{ object.content }}
</div>

</body>
</html>

터미널 테스트 결과 : OK


CSS 파일이 적용 안되어있으므로 nav 테마가 이상하게 보임.
CSS 추가가 필요.

여기까지 코드 정리

tests.py

from django.test import TestCase, Client
from bs4 import BeautifulSoup
from .models import Post
from django.utils import timezone
from django.contrib.auth.models import User

def create_post(title, content, author):
    blog_post = Post.objects.create(
        title=title,
        content=content,
        created=timezone.now(),
        author=author
    )
    return blog_post


class TestView(TestCase):
    # Client 의 역할을 제공해줌
    def setUp(self):
        self.client = Client()
        self.author_000 = User.objects.create(username='smith', password='nopassword')

    # Navigation Bar를 체크하는 함수
    def check_navbar(self, soup):
        navbar = soup.find('div', id='navbar')
        self.assertIn('Blog', navbar.text)
        self.assertIn('About me', navbar.text)

    # test_ 로 시작하는게 국룰. 왠만하면 변경하지 말기
    def test_post_list(self):
        response = self.client.get('/blog/')
        self.assertEqual(response.status_code, 200)

        # title 태그의 값을 잘 읽어오는지 테스트
        soup = BeautifulSoup(response.content, 'html.parser')
        title = soup.title
        print(title.text)
        self.assertEqual(title.text, 'Blog')

        # navigation bar가 잘 동작하는지 Blog, About me가 있는지 확인
        # post_list.html의 navigation 영역에 id="navbar" 추가
        self.check_navbar(soup)
        # navbar = soup.find('div', id='navbar')
        # self.assertIn('Blog', navbar.text)
        # self.assertIn('About me', navbar.text)

        # 게시글이 없는경우 = object 의 갯수가 하나도 없는경우
        self.assertEqual(Post.objects.count(), 0)
        self.assertIn('아직 게시물이 없습니다', soup.body.text)

        # blog_post 를 create 하는 부분을 함수로 만들자
        post_000 = create_post(
            title='The first Post',
            content='Hello world. We are the world',
            author=self.author_000,
        )

        # 게시글이 있는경우 = 게시글 수가 0보다 큰 경우
        self.assertGreater(Post.objects.count(), 0)

        # refresh를 했을때 게시물이 없는 상태로 가면 안되므로...
        response = self.client.get('/blog/')
        self.assertEqual(response.status_code, 200)
        soup = BeautifulSoup(response.content, 'html.parser')
        body = soup.body
        # 게시글이 있는 상태인데, 아직 게시물이 없다고 하면 안되므로 NotIn 사용
        self.assertNotIn('아직 게시물이 없습니다', body.text)
        # 있는 게시글 post_000 의 타이틀을 내놓으면 됨
        self.assertIn(post_000.title, body.text)

    # 여기에도 Nav bar가 있나 테스트하기
    def test_post_detail(self):
        post_000 = create_post(
            title='The first Post',
            content='Hello world. We are the world',
            author=self.author_000,
        )

        self.assertGreater(Post.objects.count(), 0)
        # getAbsolute URL
        post_000_url = post_000.get_absolute_url()
        self.assertEqual(post_000_url, '/blog/{}/'.format(post_000.pk))

        # 페이지 열어봐야지
        response = self.client.get(post_000_url)
        self.assertEqual(response.status_code, 200)

        # {} - Blog  의 형태로 보여주고 싶다
        soup = BeautifulSoup(response.content, 'html.parser')
        title = soup.title
        self.assertEqual(title.text, '{} - Blog'.format(post_000.title))

        self.check_navbar(soup)

post_detail.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ object.title }} - Blog</title>
</head>
<body>
<div class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary" id="navbar">
    <div class="container">
        <a href="../" class="navbar-brand">Lab. Rxdryd</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarResponsive">
            <ul class="navbar-nav">
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" id="themes">Themes <span class="caret"></span></a>
                    <div class="dropdown-menu" aria-labelledby="themes">
                        <a class="dropdown-item" href="../default/">Default</a>
                        <div class="dropdown-divider"></div>
                        <a class="dropdown-item" href="../cerulean/">Cerulean</a>
                        <a class="dropdown-item" href="../cosmo/">Cosmo</a>
                        <a class="dropdown-item" href="../cyborg/">Cyborg</a>
                        <a class="dropdown-item" href="../darkly/">Darkly</a>
                        <a class="dropdown-item" href="../flatly/">Flatly</a>
                        <a class="dropdown-item" href="../journal/">Journal</a>
                        <a class="dropdown-item" href="../litera/">Litera</a>
                        <a class="dropdown-item" href="../lumen/">Lumen</a>
                        <a class="dropdown-item" href="../lux/">Lux</a>
                        <a class="dropdown-item" href="../materia/">Materia</a>
                        <a class="dropdown-item" href="../minty/">Minty</a>
                        <a class="dropdown-item" href="../morph/">Morph</a>
                        <a class="dropdown-item" href="../pulse/">Pulse</a>
                        <a class="dropdown-item" href="../quartz/">Quartz</a>
                        <a class="dropdown-item" href="../sandstone/">Sandstone</a>
                        <a class="dropdown-item" href="../simplex/">Simplex</a>
                        <a class="dropdown-item" href="../sketchy/">Sketchy</a>
                        <a class="dropdown-item" href="../slate/">Slate</a>
                        <a class="dropdown-item" href="../solar/">Solar</a>
                        <a class="dropdown-item" href="../spacelab/">Spacelab</a>
                        <a class="dropdown-item" href="../superhero/">Superhero</a>
                        <a class="dropdown-item" href="../united/">United</a>
                        <a class="dropdown-item" href="../vapor/">Vapor</a>
                        <a class="dropdown-item" href="../yeti/">Yeti</a>
                        <a class="dropdown-item" href="../zephyr/">Zephyr</a>
                    </div>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/blog/">Blog</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/about_me/">About me</a>
                </li>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" id="download">Morph <span class="caret"></span></a>
                    <div class="dropdown-menu" aria-labelledby="download">
                        <a class="dropdown-item" rel="noopener" target="_blank" href="https://jsfiddle.net/bootswatch/f4torgy7/">Open in JSFiddle</a>
                        <div class="dropdown-divider"></div>
                        <a class="dropdown-item" href="../5/morph/bootstrap.min.css" download>bootstrap.min.css</a>
                        <a class="dropdown-item" href="../5/morph/bootstrap.css" download>bootstrap.css</a>
                        <div class="dropdown-divider"></div>
                        <a class="dropdown-item" href="../5/morph/_variables.scss" download>_variables.scss</a>
                        <a class="dropdown-item" href="../5/morph/_bootswatch.scss" download>_bootswatch.scss</a>
                    </div>
                </li>
            </ul>
            {#            <ul class="navbar-nav ms-md-auto">#}
            {#                <li class="nav-item">#}
            {#                    <a target="_blank" rel="noopener" class="nav-link" href="https://github.com/thomaspark/bootswatch/"><i class="fa fa-github"></i> GitHub</a>#}
            {#                </li>#}
            {#                <li class="nav-item">#}
            {#                    <a target="_blank" rel="noopener" class="nav-link" href="https://twitter.com/bootswatch"><i class="fa fa-twitter"></i> Twitter</a>#}
            {#                </li>#}
            {#            </ul>#}
        </div>
    </div>
</div>
<h1>{{ object.title }}</h1>
<div>
    {{ object.content }}
</div>

</body>
</html>

models.py

from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=30)
    content = models.TextField()

    head_image = models.ImageField(upload_to='blog/%Y/%m/%d/', blank=True)

    created = models.DateTimeField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return '{} :: {}'.format(self.title, self.author)

    def get_absolute_url(self):
        return '/blog/{}/'.format(self.pk)

참고

https://www.inflearn.com/course/%ED%8C%8C%EC%9D%B4%EC%8D%AC/dashboard

profile
정리데쓰

0개의 댓글