<div class="col-lg-8">
{% block content %}
{% endblock %}
</div>
column-12 중 8 에 해당하는 부분을 제외한 공통영역 분리
col-8 에 해당하는 영역에 {% block content %}, {% endblock %}
로 감싸주고 {% block content %}
위 전체, {% endblock %}
의 아래 전체코드 삭제
{% extends 'blog/base.html' %}
를 맨위에 선언해주기
base.html 을 확장해서 표현하겠다는 의미
{% extends 'blog/base.html' %}
{% block content %}
<h1 class="my-4">Blog</h1>
{% 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 %}
{% endblock %}
post_list.html 일때는 Blog 만, post_detail.html 일때는 Title - Blog 로 표시해줘야함.
<title>{% block title %}Blog{% endblock %}</title>
{% extends 'blog/base.html' %}
{% block title %}{{ object.title }} - Blog{% endblock %}
{% block content %}
<h1>{{ object.title }}</h1>
<div>
{{ object.content }}
</div>
{% endblock %}
터미널 테스트 결과 : 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 70, in test_post_list
self.assertEqual(post_000_read_more_btn['href'], post_000.get_absolute_url())
AssertionError: '#!' != '/blog/1/'
- #!
+ /blog/1/
<a class="btn btn-primary" href="#!" id="read-more-post-{{ p.pk }}">Read more →</a>
# test_ 로 시작하는게 국룰. 왠만하면 변경하지 말기
def test_post_list(self):
...
...
# read more 버튼 클릭
post_000_read_more_btn = body.find('a', id='read-more-post-{}'.format(post_000.pk))
self.assertEqual(post_000_read_more_btn['href'], post_000.get_absolute_url())
<a class="btn btn-primary" href="{{ p.get_absolute_url }}" id="read-more-post-{{ p.pk }}">
p.get_absolute_url
이 함수임에도 불구하고 html에서는 뒤에() 안써줌
터미널 테스트 수행 : OK
Read more 버튼 클릭시 detail 페이지로 잘 넘어간다.
https://startbootstrap.com 의
https://startbootstrap.com/previews/blog-post 에서
col-8 에 해당되는 부분만 긁어다 post_detail.html 의 block 안에 넣기
col-8 영역을 main_div로, col-4 영역을 sid_div로 칭하자
<div class="col-lg-8" id="main_div">
...
<div class="col-lg-4" id="sid_div">
body = soup.body
# main div 검사
main_div = body.find('div', id='main_div')
self.assertIn(post_000.title, main_div.text)
self.assertIn(post_000.author.username, main_div.text)
<!-- Post header-->
<h1>{{ object.title }}</h1>
<!-- Author -->
<p class="lead">
by
<a href="#">{{ object.author.username }}</a>
</p>
{% extends 'blog/base.html' %}
{% block title %}{{ object.title }} - Blog{% endblock %}
{% block content %}
<!-- Post content-->
<article>
<!-- Post header-->
<h1>{{ object.title }}</h1>
<!-- Author -->
<p class="lead">
by
<a href="#">{{ object.author.username }}</a>
</p>
<hr>
<!-- Date/Time -->
<p>Posted on Jan 1, 2021</p>
<hr>
<!-- Preview image figure-->
<figure class="mb-4"><img class="img-fluid rounded" src="https://dummyimage.com/900x400/ced4da/6c757d.jpg" alt="..." /></figure>
<!-- Post content-->
<section class="mb-5">
<p class="fs-5 mb-4">Science is an enterprise that should be cherished as an activity of the free human mind. Because it transforms who we are, how we live, and it gives us an understanding of our place in the universe.</p>
<p class="fs-5 mb-4">The universe is large and old, and the ingredients for life as we know it are everywhere, so there's no reason to think that Earth would be unique in that regard. Whether of not the life became intelligent is a different question, and we'll see if we find that.</p>
<p class="fs-5 mb-4">If you get asteroids about a kilometer in size, those are large enough and carry enough energy into our system to disrupt transportation, communication, the food chains, and that can be a really bad day on Earth.</p>
<h2 class="fw-bolder mb-4 mt-5">I have odd cosmic thoughts every day</h2>
<p class="fs-5 mb-4">For me, the most fascinating interface is Twitter. I have odd cosmic thoughts every day and I realized I could hold them to myself or share them with people who might be interested.</p>
<p class="fs-5 mb-4">Venus has a runaway greenhouse effect. I kind of want to know what happened there because we're twirling knobs here on Earth without knowing the consequences of it. Mars once had running water. It's bone dry today. Something bad happened there as well.</p>
</section>
</article>
<!-- Comments section-->
<section class="mb-5">
<div class="card bg-light">
<div class="card-body">
<!-- Comment form-->
<form class="mb-4"><textarea class="form-control" rows="3" placeholder="Join the discussion and leave a comment!"></textarea></form>
<!-- Comment with nested comments-->
<div class="d-flex mb-4">
<!-- Parent comment-->
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.
<!-- Child comment 1-->
<div class="d-flex mt-4">
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors.
</div>
</div>
<!-- Child comment 2-->
<div class="d-flex mt-4">
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
When you put money directly to a problem, it makes a good headline.
</div>
</div>
</div>
</div>
<!-- Single comment-->
<div class="d-flex">
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence.
</div>
</div>
</div>
</div>
</section>
{% endblock %}
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)
# 게시글이 없는경우 = 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)
# read more 버튼 클릭
post_000_read_more_btn = body.find('a', id='read-more-post-{}'.format(post_000.pk))
self.assertEqual(post_000_read_more_btn['href'], post_000.get_absolute_url())
# 여기에도 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)
body = soup.body
# main div 검사
main_div = body.find('div', id='main_div')
self.assertIn(post_000.title, main_div.text)
self.assertIn(post_000.author.username, main_div.text)
self.assertIn(post_000.content, main_div.text)
{% extends 'blog/base.html' %}
{% block title %}{{ object.title }} - Blog{% endblock %}
{% block content %}
<!-- Post content-->
<!-- Post header-->
<h1>{{ object.title }}</h1>
<!-- Author -->
<p class="lead">
by
<a href="#">{{ object.author.username }}</a>
</p>
<hr>
<!-- Date/Time -->
<p>Posted on {{ object.created }}</p>
<hr>
<!-- Preview image figure-->
{% if object.head_image %}
<img class="img-fluid rounded" src="https://dummyimage.com/900x400/ced4da/6c757d.jpg" alt="..." />
{% endif %}
<!-- Post content-->
{{ object.content }}
<!-- Comments section-->
<section class="mb-5">
<div class="card bg-light">
<div class="card-body">
<!-- Comment form-->
<form class="mb-4"><textarea class="form-control" rows="3" placeholder="Join the discussion and leave a comment!"></textarea></form>
<!-- Comment with nested comments-->
<div class="d-flex mb-4">
<!-- Parent comment-->
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
If you're going to lead a space frontier, it has to be government; it'll never be private enterprise. Because the space frontier is dangerous, and it's expensive, and it has unquantified risks.
<!-- Child comment 1-->
<div class="d-flex mt-4">
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
And under those conditions, you cannot establish a capital-market evaluation of that enterprise. You can't get investors.
</div>
</div>
<!-- Child comment 2-->
<div class="d-flex mt-4">
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
When you put money directly to a problem, it makes a good headline.
</div>
</div>
</div>
</div>
<!-- Single comment-->
<div class="d-flex">
<div class="flex-shrink-0"><img class="rounded-circle" src="https://dummyimage.com/50x50/ced4da/6c757d.jpg" alt="..." /></div>
<div class="ms-3">
<div class="fw-bold">Commenter Name</div>
When I look at the universe and all the ways the universe wants to kill us, I find it hard to reconcile that with statements of beneficence.
</div>
</div>
</div>
</div>
</section>
{% endblock %}
지금은 tests.py 에 TestView만 있었는데 TestModel 을 추가해보자
class Category(models.Model):
# Category 명은 중복되면 안되므로 unique
name = models.CharField(max_length=25, unique=True)
description = models.TextField(blank=True)
from django.test import TestCase, Client
from bs4 import BeautifulSoup
from .models import Post, Category
from django.utils import timezone
from django.contrib.auth.models import User
def create_category(name='life', description=None):
# Category 가 생성되어 있을경우 가져오고(get), 아니면 인자값으로 만들어라 (create)
category, is_created = Category.objects.get_or_create(
name=name,
description=description
)
return category
class TestModel(TestCase):
def setUp(self):
self.client = Client()
self.author_000 = User.objects.create(username='smith', password='nopassword')
def test_category(self):
category = create_category()
# def test_post(self):
# category = create_category(
#
# )
# post_000 = create_post(
# title='The first Post',
# content='Hello world. We are the world',
# author=self.author_000,
# category=category
# )
터미널 테스트 결과 : Fail
django.db.utils.OperationalError: no such table: blog_category
당연한것. table 을 안만들었다
일단 Model 변경되었으니 마이그레이션 수행 필요
Model이 변경되면 무조건무조건!!! 마이그레이션 해야한다
안그러면 operation Error 발생
ᐅ python manage.py makemigrations
ᐅ python manage.py migrate
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
# Category 명은 중복되면 안되므로 unique
name = models.CharField(max_length=25, unique=True)
description = models.TextField(blank=True)
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)
category = models.ForeignKey(Category, blank=True, null=True, on_delete=models.SET_NULL)
def __str__(self):
return '{} :: {}'.format(self.title, self.author)
def get_absolute_url(self):
return '/blog/{}/'.format(self.pk)
from django.test import TestCase, Client
from bs4 import BeautifulSoup
from .models import Post, Category
from django.utils import timezone
from django.contrib.auth.models import User
def create_category(name='life', description=' '):
# Category 가 생성되어 있을경우 가져오고(get), 아니면 인자값으로 만들어라 (create)
category, is_created = Category.objects.get_or_create(
name=name,
description=description
)
return category
def create_post(title, content, author, category=None):
blog_post = Post.objects.create(
title=title,
content=content,
created=timezone.now(),
author=author,
category=category,
)
return blog_post
class TestModel(TestCase):
def setUp(self):
self.client = Client()
self.author_000 = User.objects.create(username='smith', password='nopassword')
def test_category(self):
category = create_category()
post_000 = create_post(
title='The first Post',
content='Hello world. We are the world',
author=self.author_000,
category=category
)
# category 에 연결된 Class 인 post 를 불러올 수 있다. (소문자 .post로 가져옴)
self.assertEqual(category.post_set.count(), 1)
def test_post(self):
category = create_category()
post_000 = create_post(
title='The first Post',
content='Hello world. We are the world',
author=self.author_000,
category=category
)
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)
# 게시글이 없는경우 = 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)
# read more 버튼 클릭
post_000_read_more_btn = body.find('a', id='read-more-post-{}'.format(post_000.pk))
self.assertEqual(post_000_read_more_btn['href'], post_000.get_absolute_url())
# 여기에도 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)
body = soup.body
# main div 검사
main_div = body.find('div', id='main_div')
self.assertIn(post_000.title, main_div.text)
self.assertIn(post_000.author.username, main_div.text)
self.assertIn(post_000.content, main_div.text)
class PostList(ListView):
model = Post
def get_queryset(self):
return Post.objects.order_by('-created')
def get_context_data(self, *, object_list=None, **kwargs):
context = super(PostList, self).get_context_data(**kwargs)
context['category_list'] = Category.objects.all()
context['posts_without_category'] = Post.objects.filter(category=None).count()
return context
# category card에서 미분류 (1), 정치/사회 (1) 있어야함
category_card = body.find('div', id='category-card')
self.assertIn('미분류 (1)', category_card.text)
self.assertIn('정치/사회 (1)', category_card.text)
### main_div 에는 '정치/사회', '미분류' 있어야함
main_div = body.find('div', id='main_div')
self.assertIn('정치/사회', main_div.text)
self.assertIn('미분류', main_div.text)
<div class="col-sm-6">
<ul class="list-unstyled mb-0">
<li><a href="#!">미분류 ({{ posts_without_category }})</a></li>
{% for category in category_list %}
<li><a href="#!">{{ category.name }} ({{ category.post_set.count }})</a></li>
{% endfor %}
<li><a href="#!">Freebies</a></li>
</ul>
</div>
https://getbootstrap.com/docs/5.1/components/badge/
from django.contrib import admin
from .models import Post, Category
admin.site.register(Post)
admin.site.register(Category)
class Category(models.Model):
# Category 명은 중복되면 안되므로 unique
name = models.CharField(max_length=25, unique=True)
description = models.TextField(blank=True)
def __str__(self):
return self.name
<div class="card-body">
{% if p.category %}
<span class="badge bg-primary" style="float:right">{{ p.category }}</span>
{# <span class="badge bg-primary float-right">{{ p.category }}</span>#}
{% else %}
<span class="badge bg-primary" style="float:right">미분류</span>
{# <span class="badge bg-primary float-right">미분류</span>#}
{% endif %}
<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="{{ p.get_absolute_url }}" id="read-more-post-{{ p.pk }}">Read more →</a>
</div>
https://www.inflearn.com/course/%ED%8C%8C%EC%9D%B4%EC%8D%AC/dashboard