Django 개발 (5)

RXDRYD·2021년 8월 21일
1

Navgation Bar 가 post_list.html , post_detail.html 둘다에 하드코딩해야할까?

base.html 을 생성해서 공통되는 영역 분리

base.html : post_list.html 을 통 복붙

<div class="col-lg-8">
     {%  block content %}
     {%  endblock %}
</div>

column-12 중 8 에 해당하는 부분을 제외한 공통영역 분리

post_list.html

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 %}

base.html

post_list.html 일때는 Blog 만, post_detail.html 일때는 Title - Blog 로 표시해줘야함.

<title>{% block title %}Blog{% endblock %}</title>

post_detail.html

{% extends  'blog/base.html' %}

{% block title %}{{ object.title }} - Blog{% endblock %}

{% block content %}

    <h1>{{ object.title }}</h1>
    <div>
        {{ object.content }}
    </div>
{% endblock %}

Read More 클릭 시 반응하게!

TDD 규칙에 입각해서 구현해보자

터미널 테스트 결과 : 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/

post_list.html : id 추가

<a class="btn btn-primary" href="#!" id="read-more-post-{{ p.pk }}">Read more →</a>

tests.py : readmore 버튼 관련 추가

# 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())

post_list.html : href 변경

<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 페이지로 잘 넘어간다.

DetailView 디자인 개선

https://startbootstrap.com
https://startbootstrap.com/previews/blog-post 에서
col-8 에 해당되는 부분만 긁어다 post_detail.html 의 block 안에 넣기

base.html

col-8 영역을 main_div로, col-4 영역을 sid_div로 칭하자

<div class="col-lg-8" id="main_div">
...
<div class="col-lg-4" id="sid_div">

tests.py : main_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_detail.html

<!-- Post header-->
<h1>{{ object.title }}</h1>

<!-- Author -->
<p class="lead">
    by
    <a href="#">{{ object.author.username }}</a>
</p>

post_detail.html 전체코드

{% 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 %}

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)

        # 게시글이 없는경우 = 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)

post_detail.html 전체코드

{% 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 %}

ForeignKey

  • Many to one
  • 여러개의 post가 하나의 user에 연결됨
  • Post는 하나의 Category에만 들어가게 구현
  • Post 가 여러개의 Category를 갖고싶은 경우 -> 다대다구조 Tag

Category

지금은 tests.py 에 TestView만 있었는데 TestModel 을 추가해보자

models.py : Category 클래스 생성

class Category(models.Model):
    # Category 명은 중복되면 안되므로 unique
    name = models.CharField(max_length=25, unique=True)
    description = models.TextField(blank=True)

tests.py : Catecory 모델 추가

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

models.py 전체코드

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)

tests.py 전체 코드 : description=''

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)

views.py : category (숫자) 가져오고 싶어

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

tests.py : 포스트 하나 더 생성 후 category 체크

# 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)

base.html : category 부분

<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>

bootstrap 에서 Badge 가져오기

https://getbootstrap.com/docs/5.1/components/badge/

admin.py : Category 추가

from django.contrib import admin
from .models import Post, Category


admin.site.register(Post)
admin.site.register(Category)

models.py : str return

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

post_list.html : category Badge 추가

<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

profile
정리데쓰

0개의 댓글