Relations - 다대일 관계
210801, 210803
다대일 관계 - author (작성자) 추가하기
다대일 관계
A모델의 여러 레코드가 B모델의 한 레코드에 연결될 수 있는 관계
한 유저가 여러개의 포스트를 작성했을 때 포스트라는 여러 레코드가 유저라는 한 레코드에 연결된다
위 관계를 가지는 작성자 필드를 추가할 것임
models.py
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가지의 속성을 설정할 수 있다.
여기 참고
이 때 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
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
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
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
를 추가해준다.
그러면 다음과 같이 바뀐 모습을 볼 수 있다.

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

그 외에도 이런것들이 된다.
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
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가 있을때와 없을 때로 나누어준다.
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

위 페이지의 카테고리 박스처럼 하려고한다. 이 때 이 항목은 Model의 Category를 참조해야한다. 또, Post 개수에 대한 정보도 얻어야 한다. 이는 일단 여기서 먼저 손보자
views.py
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
{% 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
에 카테고리 항목을 보여주는 뱃지를 추가해준다.
{% 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
도 마찬가지로 수정해준다.
<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!
카테고리 페이지 만들기


이러한 구조를 가지는 것이 목표. 이 때 이 페이지는 카테고리 카드에 있는 항목을 클릭했을 때 이동하는 페이지이다.
views.py
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
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에 페이지가 새로운 템플릿 없이 구성된 것인지 잘 모르겠다.. 아마 기존 템플릿에 조건문으로 덮어쓴 것 같다는 예상 뿐... ㅠㅠ
Last updated
Was this helpful?