# Relations - 다대일 관계

## 다대일 관계 - author (작성자) 추가하기

#### 다대일 관계

A모델의 여러 레코드가 B모델의 한 레코드에 연결될 수 있는 관계

* 한 유저가 여러개의 포스트를 작성했을 때 포스트라는 여러 레코드가 유저라는 한 레코드에 연결된다

위 관계를 가지는 작성자 필드를 추가할 것임

`models.py`

```python
from django.contrib.auth.models import User

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    
def __str__(self):
    return f"{self.pk} {self.title} :: {self.author}"    
```

* 외래키 필드를 추가한다. 이 때 `on_delete` 속성은 만약 author가 삭제되었을 경우, 해당 포스트를 삭제할 것이냐고 묻는 것이다.
  * cascade는 삭제하라는 뜻이며 6가지의 속성을 설정할 수 있다.
  * [여기](https://vallhalla-edition.tistory.com/60) 참고
* 이 때 migrations을 진행하면 다음과 같은 텍스트가 뜨는데, default값을 정해달라는 뜻이다.
  * 1번은 migration 할테니, default를 지금 정해달라.
  * 2번은 migration를 취소하고 default를 정하라

```
You are trying to add a non-nullable field 'author' to post without a default; we can't do that (the database needs
 something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option:
```

여기서는 1을 누른다.

```
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 1
```

그리고 1을 입력한다. 그러면 1번째 사용자의 이름이 기본 이름으로 설정된다.

## 연결된 레코드가 삭제될 때 동작 결정하기 - CASCADE, SETNULL

```
author = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
```

다음과 같이 `on_delete` 속성을 `SET_NULL`로 설정할 수도 있다. 이 때 반드시 `null=True`를 같이 설정해줘야 한다.

이 때 포스트를 작성한 적 있는 유저를 삭제하면 해당 포스트의 이름은 None으로 남게 된다.

## 포스트 목록, 포스트 상세 페이지에 작성자 추가하기

`tests.py` 에서는 매번 테스트를 진행할 때 마다 DB가 초기화되었다고 가정하고 실행된다. 이 때 `setUp` 함수에서 이러한 데이터를 미리 초기화해줄 수 있다.

`tests.py`

```python
from django.contrib.auth.models import User


class TestView(TestCase):
    def setUp(self):
        self.client = Client()
        self.user_trump = User.object.create_user(
            username='trump',
            password='somepassword'
        )
        self.user_obama = User.object.create_user(
            username='obama',
            password='somepassword'
        )
```

## Category 만들기

`models.py`

```python
class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=200, unique=True, allow_unicode=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'Categories'
```

* `unique` 속성은 동일한 name을 가진 카테고리가 없도록 설정하는 것
* `Meta` 클래스를 추가하고 `verbose_name_plural` 변수를 추가하면 Categorys 항목의 이름을 바꿀 수 있다
* 마이그리에션 작업 할 것

`admin.py`

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

admin.site.register(Post)


class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name',)}


admin.site.register(Category, CategoryAdmin)

```

* 관리자 계정에서 카테고리 페이지를 볼 수 있도록 추가한다.
* 이 때 `CategoryAdmin(admin.ModelAdmin)` 클래스를 추가하고 `admin.site.register` 에 이 클래스를 인자로 받으면 category name이 입력될 때 slug도 동일하게 같이 입력되게 된다.

`blank = True` vs `null = True`

* blank = True : 데이터 폼을 작성할 때 필수적이어야 되는지에 대한 선택지
* null = True : 데이터베이스에 마이그레이트 될 때 해당 필드가 필수적이어야 되는지에 대한 선택지
* author를 설정하고, author가 탈퇴해서 해당 게시물의 author가 None 이 더라도 null = True로 되어있으면 상관없다.

## django shell로 다대일구조 연결 확인

기존 쉘은 다음과 같다

```
python manage.py shell

Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)

>>> from blog.models import Post, Category
>>> Post.objects.count()
5
>>> Category.objects.count()
4

>>> for p in Post.objects.all():
...     print(p)
...
1 new post :: bini
2 second post :: mini
3 third post :: mini
4 Meteor :: mini
5 여름이 오면 서핑을 하고싶어요 :: bini
>>> for c in Category.objects.all():
...     print(c)
...
programming
culture&art
game
entertainment
```

무언가 칙칙하게 느껴질 수 있다. 이 때 쓸 수 있는 것이 있다. 일단

`pip install django_extensions, ipython` 을 한다. 그리고 `settings.py` 의 `INSTALLED_APPS` 에 `django_extensions` 를 추가해준다.

그러면 다음과 같이 바뀐 모습을 볼 수 있다.

![](/files/-MgAuU9JMXyC0Z0b1TGV)

* 일단 색이 추가되어서 가독성이 좋아졌다.
* 그리고 따로 import를 하지 않아도 된다.

![](/files/-MgAuhOUYmmhuPO6zQkj)

그 외에도 이런것들이 된다.

```
In [5]: category_programming = Category.objects.get(slug='programming')

In [6]: category_programming
Out[6]: <Category: programming>

In [7]: category_programming = Category.objects.get(name__startswith='c')

In [8]: category_programming
Out[8]: <Category: culture&art>

In [9]: for p in category_programming.post_set.all():
    ...:     print(p)
    ...: 
5 여름이 오면 서핑을 하고싶어요 :: bini
```

## 포스트 목록 페이지 수정하기 1

일단 지금까지 작성한 `tests.py` 를 정리해보자

첫번째, 각 테스트마다 포스트를 만들었다. 네비게이션도 한번에 관리하기 위해서 함수로 만들었는데 포스트도 동일하게 관리할 수 있지 않을까? 이 때 카테고리까지 고려해서 관리해보자

따라서 SetUp을 다음과 같이 수정한다.

`tests.py`

```python
class TestView(TestCase):
    def setUp(self):
        self.client = Client()
        self.user_trump = User.objects.create_user(
            username='trump',
            password='somepassword'
        )
        self.user_obama = User.objects.create_user(
            username='obama',
            password='somepassword'
        )
        self.category_programming = Category.objects.create(
            name='programming',
            slug='programming'
        )
        self.category_music = Category.objects.create(
            name='music',
            slug='music'
        )

        self.post_001 = Post.objects.create(
            title='첫번째 포스트 입니다.',
            content='Hello, World. We are the World.',
            author=self.user_trump,
            category=self.category_programming,
        )
        self.post_002 = Post.objects.create(
            title='두번째 포스트 입니다.',
            content='안녕 여러분, 나도 여러분의 일부야.',
            author=self.user_obama,
            category=self.category_music,
        )
        self.post_003 = Post.objects.create(
            title='세번째 포스트 입니다.',
            content='카테고리 없어.',
            author=self.user_obama,
        )
```

그리고, 이후에 나오는 post에 관한 코드들을 self.post로 바꾸어준다.

이제, 포스트가 3개로 고정되었다. 따라서 test를 할 때 post가 있을때와 없을 때로 나누어준다.

```python
def test_post_list_without_posts(self):
    Post.objects.all().delete()
    self.assertEqual(Post.objects.count(), 0)

    response = self.client.get('/blog/')
    self.assertEqual(response.status_code, 200)

    soup = BeautifulSoup(response.content, 'html.parser')
    self.navbar_Test(soup)
    self.assertIn('상만두', soup.title.text)

    main_area = soup.find('div', id='main-area')
    self.assertIn('아직 게시물이 없습니다.', main_area.text)

def test_post_list_with_posts(self):
    self.assertEqual(Post.objects.count(), 3)

    response = self.client.get('/blog/')
    self.assertEqual(response.status_code, 200)

    soup = BeautifulSoup(response.content, 'html.parser')
    self.assertIn('상만두', soup.title.text)

    self.navbar_Test(soup)

    main_area = soup.find('div', id='main-area')
    self.assertIn(self.post_001.title, main_area.text)
    self.assertIn(self.post_002.title, main_area.text)
    # 3.4 "아직 게시물이 없습니다" 라는 문구가 없어야 한다
    self.assertNotIn('아직 게시물이 없습니다.', main_area.text)

    self.assertIn(self.post_001.author.username.upper(), main_area.text)
    self.assertIn(self.post_002.author.username.upper(), main_area.text)

```

## 포스트 목록 페이지 수정하기 2

![](/files/-MgB3JInD5BgyE0QJvMD)

위 페이지의 카테고리 박스처럼 하려고한다. 이 때 이 항목은 Model의 Category를 참조해야한다. 또, Post 개수에 대한 정보도 얻어야 한다. 이는 일단 여기서 먼저 손보자

`views.py`

```python
from .models import Post, Category

class PostList(ListView):
    model = Post
    ordering = '-pk'

    def get_context_data(self, **kwargs):
        context = super(PostList, self).get_context_data()
        context['categories'] = Category.objects.all()
        context['no_category_post_count'] = Post.objects.filter(category=None).count()
        return context
```

다음과 같이 함수를 추가해줘서, 카테고리 정보를 가져올 수 있도록 한다.

그리고 실제로 템플릿에서도 보일 수 있도록 수정해준다

`base.html`

```markup
{% for category in categories %}
<li>
    <a href="#!"> {{ category.name }} ({{ category.post_set.count }})</a>
</li>
{% endfor %}
<li>
    <a href="#!"> 미분류 ({{ no_category_post_count }})</a>
</li>
```

그리고 `post_list.html` 에 카테고리 항목을 보여주는 뱃지를 추가해준다.

```markup
{% if p.category %}
    <span class="badge badge-success float-right"> {{ p.category }}</span>
{% else %}
    <span class="badge badge-success float-right"> 미분류 </span>
{% endif %}
```

* 이 때 float-right는 오른쪽으로 정렬되게 한다.
  * 부트스트랩에서 지원하는 기능

## 포스트 상세 페이지 수정하기

`post_detail.html` 도 마찬가지로 수정해준다.

```markup
<div id="post-area">
    {% if post.category %}
    <span class="badge badge-success float-right"> {{ post.category }}</span>
    {% else %}
    <span class="badge badge-success float-right"> 미분류 </span>
    {% endif %}
```

늘 주의할 것은 p.category 가 아니라 post.category!

## 카테고리 페이지 만들기

![](/files/-MgBB4ibRFuIS4fubxh8)

![](/files/-MgBB7MIMCCeDz-9PvNB)

이러한 구조를 가지는 것이 목표. 이 때 이 페이지는 카테고리 카드에 있는 항목을 클릭했을 때 이동하는 페이지이다.

`views.py`

```python
def category_page(request, slug):
    if slug == "no_category":
        category = '미분류'
        post_list = Post.objects.filter(category=None)
    else:
        category = Category.objects.get(slug=slug)
        post_list = Post.objects.filter(category=category)
    return render(
        request,
        'blog/post_list.html',
        {
            'post_list': post_list,
            'categories': Category.objects.all(),
            'no_category_post_count': Post.objects.filter(category=None).count(),
            'category': category
        }
    )
```

이전에 `index.html`을 만드는 것 처럼 class가 아니라 함수로 만들어준다. 이 때 `slug`가 `미분류`일때의 `category`와 `post_list`를 따로 정의해줘야 한다.

`models.py`

```python
class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=200, unique=True, allow_unicode=True)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return f'/blog/category/{self.slug}/'
```

위와 같이 Post class와 동일하게 `get_absolute_url` 함수를 정의해준다. 이 때 주소는 `/blog/category/slug/` 가 될 수 있도록 한다.

근데, 나도 어떻게 새로운 url에 페이지가 새로운 템플릿 없이 구성된 것인지 잘 모르겠다.. 아마 기존 템플릿에 조건문으로 덮어쓴 것 같다는 예상 뿐... ㅠㅠ


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sangmandu.gitbook.io/til/til_python_and_math/do_it_django+bootstrap/10.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
