Model: 장고에서 데이터 구조를 정의하고 데이터를 활용하는 부분
데이터베이스: 컴퓨터에 체계적으로 저장되는 데이터의 모음
Database Management System (DBMS): 데이터베이스를 관리해주는 프로그램
Structured Query Language (SQL): 데이터베이스와 소통할 때 쓰는 언어
Foreign Key: 테이블과 테이블 간의 관계를 나타낼 때
관계의 종류
Object-Relational Mapper (ORM): 파이썬의 object를 데이터베이스의 테이블 형태로 맵핑
python manage.py makemigrations
python manage.py migrate
Migration: 장고 모델에 정의된 내용을 데이터베이스 테이블로 옮겨주는 과정 (2단계 프로세스)
마이그레이션은 App 단위로 관리
프로젝트의 앱마다 앱에 있는 모델들에 대해서 마이그레이션 파일을 관리
어떤 마이그레이션이 적용됐는지 확인하고 싶을 때
python manage.py showmigrations
특정 앱의 마이그레이션 상태 확인하기
python manage.py showmigrations {app 이름}
python manage.py showmigrations coplate
특정 앱의 특정 마이그레이션 실행하기
python manage.py migrate {app 이름} {migration 파일 #}
python manage.py migrate coplate 0001
마이그레이션 파일 직접 만들기
python manage.py makemigrations --name "{파일 이름}"
python manage.py makemigrations --name "review_alter_title_max_length"
fixture(데이터가 장고가 알아볼 수 있는 형태로 저장)를 데이터베이스에 넣기
python manage.py loaddata {fixture_name}
python manage.py loaddata data.json
SQLite를 사용해 db.sqlite3의 테이블 확인하기
장고의 데이터베이스 쉘
python manage.py dbshell
데이터베이스에 있는 모든 테이블 리스트하기
.tables
데이터를 조회할 때 column에 대한 정보도 조회하겠다
.headers on
테이블의 column 자체에 대한 정보 보기
PRAGMA table_info('coplate_review');
테이블 데이터 조회
유저 테이블에 있는 모든 데이터 조회
SELECT * FROM coplate_user;
유저 테이블의 특정 column들만 조회
SELECT email, nickname FROM coplate_user;
데이터 필터하기
SELECT email, nickname FROM coplate_user WHERE id=1;
dbshell 종료
.exit
migration 6번까지 적용된 상태에서 migration 5번까지로 돌아가기
python manage.py migrate coplate 0005
migration 6번 파일 삭제하기: 파일 삭제
특정 앱의 모든 migration 취소하기
python manage.py migrate coplate zero
: 특정 migration이 다른 migration에게 의존
makemigrations python manage.py makemigrations [app_label]
python manage.py makemigrations --name "custom_name"
--name
옵션을 사용하지 않으면 장고가 알아서 이름을 정해주는데 가끔 auto
로 시작하는 의미 없는 이름을 사용migrate python manage.py migrate [app_label] [migration_name]
python manage.py migrate {app_name}
python manage.py migrate {app_name} {migration_number}
python manage.py migrate {app_name} {prev_migration_number} #migration 되돌리기
python manage.py migrate zero #앱의 모든 migration 취소
showmigrations python manage.py showmigrations [app_label]
Migration Dependency: 특정 migration이 다른 migration에 의존
취소한 migration을 다시 migrate한다고 삭제된 데이터가 다시 생성되지는 않음
테이블에 새로운 필드 추가하기
데이터 마이그레이션: 데이터를 다루는 마이그레이션 (데이터 삽입, 수정 등)
python manage.py makemigrations --name "user_email_domain"
#빈 migration 파일 만들기
python manage.py makemigrations **--empty** coplate --name "populate_email_domain"
# Generated by Django 4.1 on 2022-08-24 08:09
from django.db import migrations
def save_email_domain(apps, schema_editor):
User = apps.get_model('coplate', 'User')
for user in User.objects.all():
user.email_domain = user.email.split('@')[1]
user.save()
class Migration(migrations.Migration):
dependencies = [
("coplate", "0006_user_email_domain"),
]
operations = [
migrations.RunPython(save_email_domain),
]
# Generated by Django 4.1 on 2022-08-24 08:09
from django.db import migrations
def save_email_domain(apps, schema_editor):
User = apps.get_model('coplate', 'User')
for user in User.objects.all():
user.email_domain = user.email.split('@')[1]
user.save()
class Migration(migrations.Migration):
dependencies = [
("coplate", "0006_user_email_domain"),
]
operations = [
migrations.RunPython(save_email_domain, **migrations.RunPython.noop**),
]
class User(AbstractUser):
...
email_domain = models.CharField(max_length=30, null=True)
python manage.py makemigrations --empty coplate --name "populate_email_domain"
from django.db import migrations
def save_email_domain(apps, schema_editor):
User = apps.get_model('coplate', 'User')
for user in User.objects.all():
user.email_domain = user.email.split('@')[1]
user.save()
class Migration(migrations.Migration):
dependencies = [
('coplate', '0006_user_email_domain'),
]
operations = [
migrations.RunPython(save_email_domain, migrations.RunPython.noop),
]
python manage.py migrate
데이터 모델링: 서비스의 요구 사항에 맞게 데이터의 구조와 형식을 정하는 것
관계 파악하기
다이어그램:
1:N(일대다) 관계: ForeignKey 사용
#models.py
class Review(models.Model):
...
dt_created = models.DateTimeField(auto_now_add=True)
dt_updated = models.DateTiemField(auto_now=True)
**author = models.ForeignKey(User, on_delete=models.CASCADE)**
...
댓글 Model 정의하기
#models.py
class Comment(models.Model):
content = models.TextField(max_length=500, blank=False)
dt_created = models.DateTimeField(auto_now_add=True)
dt_updated = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
review = models.ForeignKey(Review, on_delete=models.CASCADE)
def __str__(self):
return self.content[:30]
유저 한 명은 프로필 하나를 가지고 프로필 하나는 유저 한 명에게 속함
models.OneToOneField(<to_model>, on_delete=…)
#models.py
class User(AbstractUser):
def __str__(self):
return self.email
class Profile(models.Model):
nickname = models.CharField(
max_length=15,
unique=True,
null=True,
validators=[validate_no_special_characters],
error_messages={'unique':'이미 사용중인 닉네임입니다.'},
)
profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')
intro = models.CharField(max_length=60, blank=True)
user = models.OneToOneField(User, on_delete=models.CASCADE)
User 모델을 User 모델과 Profile 모델로 나눌 때 데이터를 잃지 않으려면 데이터 마이그레이션 해야 함
#models.py
class User(AbstractUser):
nickname = models.CharField(
max_length=15,
unique=True,
null=True,
validators=[validate_no_special_characters],
error_messages={'unique': '이미 사용중인 닉네임입니다.'},
)
profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')
intro = models.CharField(max_length=60, blank=True)
def __str__(self):
return self.email
class Profile(models.Model):
nickname = models.CharField(
max_length=15,
unique=True,
null=True,
validators=[validate_no_special_characters],
error_messages={'unique': '이미 사용중인 닉네임입니다.'},
)
profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')
intro = models.CharField(max_length=60, blank=True)
user = models.OneToOneField(User, on_delete=models.CASCADE)
python manage.py makemigrations --name "profile"
python manage.py migrate
python manage.py makemigrations --empty coplate --name "migrate_profile_data"
#0008_migrate_profile_data.py
from django.db import migrations
def user_to_profile(apps, schema_editor):
User = apps.get_model('coplate', 'User')
Profile = apps.get_model('coplate', 'Profile')
for user in User.objects.all():
Profile.objects.create(
nickname = user.nickname,
profile_pic = user.profile_pic,
intro = user.intro,
user = user,
)
class Migration(migrations.Migration):
dependencies = [
('coplate', '0007_profile'),
]
operations = [
migrations.RunPython(user_to_profile),
]
python manage.py migrate
#models.py
class User(AbstractUser):
def __str__(self):
return self.email
#forms.py
class ProfileForm(forms.ModelForm):
class Meta:
model = User
fields = [
# 'nickname',
# 'profile_pic',
# 'intro',
]
widgets = {
# 'intro': forms.Textarea,
}
python manage.py makemigrations --name "delete_user_profile_fields"
python manage.py migrate
#0008_migrate_profile_data.py
def user_to_profile(apps, schema_editor):
User = apps.get_model('coplate', 'User')
Profile = apps.get_model('coplate', 'Profile')
for user in User.objects.all():
Profile.objects.create(
nickname = user.nickname,
profile_pic = user.profile_pic,
intro = user.intro,
user = user,
)
**def profile_to_user(apps, schema_editor):
User = apps.get_model('coplate', 'User')
Profile = apps.get_model('coplate', 'Profile')
for profile in Profile.objects.all():
user = profile.user
user.nickname = profile.nickname
user.profile_pic = profile.profile_pic
user.intro = profile.intro
user.save()**
class Migration(migrations.Migration):
dependencies = [
('coplate', '0007_profile'),
]
operations = [
migrations.RunPython(user_to_profile, **profile_to_user**),
]
python manage.py migrate coplate 0006
models.ManyToManyField(<to_model>)
좋아요와 아무 모델을 연결할 수 있는 일반적인(generic) 관계 만들기
contenttypes: 장고 어플리케이션에 사용되는 모든 모델에 대한 정보를 관리하는 앱
#models.py
class Like(models.Model):
...
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
**liked_object = GenericForeignKey('content_type', 'object_id')**
#liked_object를 사용하지 않으면
#먼저 모델 클래스를 가져오기
model_cls = like.content_type.model_class()
#object_id로 필터
model_cls.objects.get(id=object_id)
#liked_object를 사용하면
**like.liked_object**
#models.py
...
from django.contrib.contenttype.models import ContentType
from django.contrib.contenttype.fields import GenericForeignKey
...
class Like(models.Model):
dt_created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
liked_object = GenericForeignKey()
def __str__(self):
return f"({self.user}, {self.liked_object})"
일대다 관계 (1:N)
class MyModel(models.Model):
...
field_name = models.**ForeignKey**(<to_model>, on_delete=<option>, ...)
models.CASCADE
: 특정 유저가 삭제되면 그 유저를 참조하고 있는 리뷰도 모두 삭제models.PROTECT
: 유저를 참조하고 있는 리뷰가 하나라도 있으면 유저를 삭제하지 못하게 함models.SET_NULL
: 특정 유저를 삭제하면, 그 유저를 참조하고 있던 리뷰들의 author
값이 모두 NULL
로 설정. (사용하려면 ForeignKey
필드에 null=True
로 설정해 줘야 )일대일 관계 (1:1)
class MyModel(models.Model):
...
field_name = models.OneToOneField(<to_model>, on_delete=<option>, ...)
다대다 관계 (M:N)
자신과 관계를 맺을 때
# 팔로우
class User(AbstractUser):
...
following = models.ManyToManyField('self', symmetrical=False)
# 친구
class User(AbstractUser):
...
friends = models.ManyToManyField('self', symmetrical=True)
# 댓글
class Comment(models.Model):
...
parent_comment = models.ForeignKey('self') # symmetrical은 다대다 관계에서만 사용합니다.
제네릭 관계
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
class MyModel(models.Model):
...
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
메타 옵션, Meta Option: 자기 자신에 대한 설정
class Review(models.Model):
...
class Meta:
ordering = ['-dt_created'] #views.py의 ordering 모두 삭제 가능
모델에 ordering을 정의하면 views.py의 order_by, ordering 등은 모두 삭제해도 됨.
#admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User, Review, **Comment, Like**
UserAdmin.fieldsets += ('Custom fields',
{'fields': ('nickname', 'profile_pic', 'intro',**'following'**)}),
admin.site.register(User, UserAdmin)
admin.site.register(Review)
**admin.site.register(Comment)
admin.site.register(Like)**
ModelAdmin 클래스
from django.contrib import admin
admin.ModelAdmin
from django.contrib import admin
class CustomAdmin(admin.ModelAdmin):
...
Inline: 같은 줄에서
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
**from django.contrib.contenttypes.admin import GenericStackedInline**
from .models import User, Review, Comment, Like
class CommentInline(admin.StackedInline):
model = Comment
class LikeInline(**GenericStackedInline**):
model = Like
UserAdmin.fieldsets += ('Custom fields', {'fields': ('nickname', 'profile_pic', 'intro','following')}),
class ReviewAdmin(admin.ModelAdmin):
inlines= (
CommentInline, #ReviewAdmin에서 CommentInline을 사용하겠다는 뜻
LikeInline,
)
class CommentAdmin(admin.ModelAdmin):
inlnes= (
LikeInline,
)
admin.site.register(User, UserAdmin)
admin.site.register(Review, ReviewAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(Like)
#admin.py
...
class UserInline(admin.StackedInline):
model = User.following.through
fk_name = 'to_user'
verbose_name = 'Follower'
verbose_name_plural='Followers'
...
UserAdmin.inlines = (UserInline,)
...
.all(): 모델에 해당하는 모든 오브젝트 가져오기
Skill.objects.all()
.filter(): 필터 조건에 해당하는 오브젝트들을 가져옴
Skill.objects.filter(id=3)
Skill.objects.filter(title__startswith='프로그래밍 기초')
Skill.objects.filter(summary__contains='웹 개발')
Skill.objects.filter(title__startswith='프로그래밍 기초').filter(title__contains='Python')
Skill.objects.filter(title__startswith='프로그래밍 기초')**.count()**
.get(): 조건에 해당하는 오브젝트를 가져옴
skill = Skill.objects.get(id=1)
print(skill.title)
.order_by(): 쿼리셋을 정렬해 줌
-
기호를 붙이면 됨Skill.objects.order_by('dt_created')
Skill.objects.filter(summary__contains='웹 개발').order_by('dt_created')
.exists(): 쿼리셋에 오브젝트가 하나라도 있는지 확인해줌
Skill.objects.filter(title__startswith='프로그래밍 기초').exists()
Skill.objects.filter(title__contains='C++').exists()
.count(): 쿼리셋에 있는 오브젝트 개수를 세줌
Skill.objects.count()
Skill.objects.filter(title__startswith='프로그래밍 기초').count()
데이터 생성하기 .create()
id
)는 값을 주지 않아도 됨Skill.objects.create(
title='웹 퍼블리싱',
summary='웹 페이지의 가장 기본이라 할 수 있는 HTML과 CSS를 통해 내 머릿속의 아이디어를 실현해 보세요!'
)
데이터 수정하기
skill = Skill.objects.get(id=6)
skill.title = 'Web 퍼블리싱'
skill**.save()**
데이터 삭제하기 .delete()
.delete()
메소드를 호출해 주면 됨skill = Skill.objects.get(id=6)
skill.delete()
Generic View와 CRUD 연산
Template과 CRUD 연산
<Model>.objects
로 시작하는 연산을 사용할 수 없음{% for skill in skills %}
.count()
같은 메소드는 호출할 수 있음템플릿에서 메소드를 호출할 때는 괄호 ()
를 쓰지 않음
{{ skills.count }}
<Model>.objects.all()
<Model>.objects.get(...)
<Model>.objects.filter(...)
<Model>.objects.count()
Manager, 매니저: 장고 모델과 데이터베이스를 연결해 주는 인터페이스
<Model>.objects.order_by(...) #정렬된 쿼리셋 리턴
Reverse Relationship, 역관계
for comment in review.comment_set.all():
print(comment)
review.comment_set.count()
review.comment_set.filter(content__contains="안녕")
#html
{% for comment in review.comment_set.all %}
...
{% endfor %}
OneToOneField의 역관계
ManyToManyField
following 필드에 related_name으로 정의
#models.py
class User(AbstractUser):
...
following = models.ManyToManyField(
'self',
symmetrical=False,
blank=True,
**related_name="followers"**
)
...
class Review(models.Model):
...
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="reviews"
)
...
class Comment(models.Model):
...
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="comments"
)
review = models.ForeignKey(
Review,
on_delete=models.CASCADE,
related_name="comments"
)
...
class Like(models.Model):
...
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="likes"
)
...
아예 역관계가 필요 없을 때에는 related_name=”+”
GenericRelation을 import해서 역관계를 추가하고 싶은 모델에 GenericRelation 필드를 추가해주면 됨
- 리뷰, 코멘트 모델에 likes GenericRelation 추가
from django.contrib.contenttypes.fields import GenericForeignKey, **GenericRelation
...**
class Review(models.Model):
...
likes = GenericRelation("Like") #Review 모델이 Like 모델보다 먼저 정의돼서 가능
...
class Comment(models.Model):
...
likes = GenericRelation("Like")
...
review.likes, comment.likes로 부를 수 있음
GenericForeignKey는 on_delete 옵션이 없어서 리뷰/코멘트가 삭제되도 거기에 달린 좋아요는 삭제되지 않음
매니저란?
역관계, Reverse Relationship란?
제네릭 관계는?
ForeignKey와 같이 관계를 형성하는 필드도 필터 조건으로 사용 가능
ManyToManyField의 필터
필터에 역관계도 활용 가능
GenericForeignKey로는 필터 불가
related_query_name 옵션을 정의하면 필터에 사용 가능
- 필터 조건을 작성할 때만 유효
- 이걸 통해 리뷰/댓글 오브젝트에 접근은 불가
- 리뷰/댓글 오브젝트에 접근하려면 like.liked_object 해야 함
class Review(models.Model):
...
likes = GenericRelation("Like", related_query_name="review")
Like.objects.filter(review=review)
Like.objects.filter(comment_id=1)
생산과 수정: ForeignKey와 OneToOneField
class Comment(models.Model):
content = models.TextField(max_length=500, blank=False)
dt_created = models.DateTimeField(auto_now_add=True) #자동 생성
dt_updated = models.DateTimeField(auto_now=True) #자동 생성
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name="comments")
likes = GenericRelation("Like", related_query_name="comment") #역관계 생성
...
comment = Comment.objects.***create***(content=”안녕하세요", author=user1, review=review1)
comment = Comment.objects.***create***(content=”안녕하세요", author_id=1, review_id=1)
comment.author = user2
comment.author_id = 2
comment***.save()***
class Profile(models.Model):
...
user = models.OneToOneField(User, on_delete=models.CASCADE)
profile = Profile.objects.create(user=user1, ...)
profile = Profile.objects.create(user_id=1, ...)
profile.user=user2
profile.user_id=2
profile.save()
class Like(models.Model):
dt_created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="likes")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
liked_object = GenericForeignKey()
...
like = Like.objects.create(user=user1, content_type=ct1, object_id=1)
like = Like.objects.create(user=user1, content_type_id=1, object_id=1)
like.content_type_id=2
like.object_id=2
like.save()
#liked_object 사용 (id 전달 불가: content type 파악 불가해서)
like = Like.objects.create(user1, liked_object=obj1) #obj1: 어떤 리뷰/코멘트 오브젝트
like.liked_object=obj2
like.save()
ManyToManyField는 항상 비어있을 수 있음
오브젝트 생성 시에는 ManyToManyField에 값을 전달할 필요 없고 이후에 필드에 오브젝트를 추가하거나 제거
.add()와 .remove() 메소드 활용
class User(AbstractUser):
nickname = models.CharField(
...
)
profile_pic = models.ImageField(default='default_profile_pic.jpg', upload_to='profile_pics')
intro = models.CharField(max_length=60, blank=True)
following = models.ManyToManyField('self', symmetrical=False, blank=True, related_name="followers")
...
user1의 following 필드에 user2, user3, user4 추가
user1.following.add(user2, user3, user4) #user 오브젝트들 넘겨주기
user1.following.add(2, 3, 4) #id들 넘겨주기
user1.following.remove(user2, user3, user5)
user1.following.remove(2, 3, 5) #following 필드에 없는 유저가 파라미터로 전달되면 그 유저는 그냥 무시함
#ManyToMany 필드의 역관계도 똑같음
user1.followers.add(user2, user4)
user1.followers.remove(2)
삭제: .delete()
user.delete() #유저 삭제 -> 리뷰, 댓글, 좋아요 오브젝트들도 삭제
reivew.delete() #리뷰 삭제 -> 댓글, 좋아요 오브젝트들도 삭제
comment.delete() #댓글 삭제 -> 좋아요 오브젝트들도 삭제
필터에 관계/역관계 활용하기
# 1. user가 작성한 리뷰들 필터
Review.objects.filter(author=user)
# 2. id 1을 가지고 있는 유저가 작성한 리뷰들 필터
Review.objects.filter(author__id=1)
# 3. jonghoon이라는 닉네임을 가지고 있는 유저가 작성한 리뷰들 필터
Review.objects.filter(author__nickname="jonghoon")
# 4. 이메일이 codeit.com으로 끝나는 유저들이 작성한 리뷰들 필터
Review.objects.filter(author__email__endswith='codeit.com')
# 5. jonghoon이라는 유저가 작성한 리뷰에 달린 코멘트들 필터
Comment.objects.filter(review__author__nickname='jonghoon')
#역관계 활용
# '코스버거' 레스토랑에 대한 리뷰를 작성한 유저들 필터
User.objects.filter(**reviews**__restaurant_name='코스버거')
GenericForeignKey는?
GenericRelation
필드에 related_query_name
을 설정해야 함# 특정 리뷰에 속해있는 좋아요들 필터
Like.objects.filter(review=review)
Like.objects.filter(review__id=1)
# 특정 댓글에 속해있는 좋아요들 필터
Like.objects.filter(comment=comment)
Like.objects.filter(comment__id=1)
관계 필드가 있는 오브젝트 생성/수정
생성
comment = Comment.objects.create(content="안녕하세요", author=user1, review=review1)
# 또는
comment = Comment.objects.create(content="안녕하세요", author_id=1, review_id=1)
수정
# author 필드 수정
comment.author = user2
# 또는
comment.author_id = 2
# review 필드 수정
comment.review = review2
# 또는
comment.review_id = 2
# 오브젝트 저장
comment.save()
생성
profile = Profile.objects.create(nickname="testuser", user=user1)
# 또는
profile = Profile.objects.create(nickname="testuser", user_id=1)
수정
profile.user = user2
# 또는
profile.user_id = 2
profile.save()
항상 비어있을 수 없음
오브젝트가 생성된 후에 값을 추가
값을 추가/삭제할 때: .add(), .remove()
추가
user1.following.add(user2, user3, user4) # 유저 오브젝트들 넘겨주기
user1.following.add(2, 3, 4) # id들 넘겨주기
삭제
user1.following.remove(user2, user3, user5)
user1.following.remove(2, 3, 5)
역관계
# followers 추가
user1.followers.add(user2, user4)
user1.followers.add(2, 4)
# followers 제거
user1.followers.remove(user2)
user1.followers.remove(2)
GenericForeignKey 오브젝트
ContentType과 오브젝트 id
# ContentType과 오브젝트 id 넘겨주기
like = Like.objects.create(user=user, content_type=ct, object_id=1)
like = Like.objects.create(user=user, content_type_id=1, object_id=1)
OR GenericForeignKey
# GenericForeignKey 오브젝트 넘겨주기
like = Like.objects.create(user=user, liked_object=obj1)
like.content_type_id = 2
like.object_id = 2
like.save()
like.liked_object = obj2
like.save()
관계 필드가 있는 오브젝트 삭제: .delete()
<div class="like-comment-block">
<div class="like-comment-header">
<button class="like-button">
<img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
<span> 좋아요 {{ review.likes.count }}</span>
</button>
<div class="comment-info">
<img src="{% static 'coplate/icons/ic-comment.svg' %}" alt="comment icon">
<span> 댓글 {{ review.comments.count }}</span>
</div>
</div>
<div class="comment-list">
{% for comment in review.comments.all %}
<div class="comment">
<div class="comment-header">
<a href="{% url 'profile' comment.author.id %}">
<div class="author">
<div class="cp-avatar" style="background-image: url('{{ comment.author.profile_pic.url }}')"></div>
<span>{{ comment.author.nickname }}</span>
</div>
</a>
{% if user == comment.author %}
<div class="buttons">
<a href="#">삭제</a>
<span> | </span>
<a href="#">수정</a>
</div>
{% endif %}
</div>
<div class="comment-content">
{{ comment.content|linebreaksbr }}
</div>
<div class="comment-footer">
<div class="comment-date">
{{ comment.dt_created|date:"Y년 n월 j일" }}
</div>
<button class="like-button">
<img width="16px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
<span> 좋아요 {{ comment.likes.count }}</span>
</button>
</div>
</div>
{% endfor %}
</div>
</div>
#forms.py
...
from .models import User, Review, **Comment
...**
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = [
'comment',
]
widgets = {
'comment': forms.Textarea,
}
#views.py
...
from .forms import ReviewForm, ProfileForm, **CommentForm**
...
class ReviewDetailView(DetailView):
...
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = CommentForm()
return context
#review_detail.html
...
{% load widget_tweaks %}
...
<form class="comment-create-form" action="#" method="post">
{% csrf_token %}
{% if user.is_authenticated %}
{{ form.content|attr:"placeholder:댓글 내용을 입력해주세요."|add_class:"cp-input" }}
<button class="cp-button small" type="submit">등록</button>
{% else %}
<a href="{% url 'account_login' %}**?next=**{% url 'review-detail' review.id %}">
{{ form.content|attr:"placeholder:댓글을 작성하려면 로그인이 필요합니다."|add_class:"cp-input"|**attr:"disabled"** }}
</a>
<button class="cp-button small **secondary**" type="submit" **disabled**>등록</button>
{% endif %}
</form>
...
#urls.py
...
urlpatterns = [
...
# comment
path('reviews/<int:review_id>/comments/create/', views.CommentCreateView.as_view(), name='comment-create'),
]
...
<form class="comment-create-form" **action="{% url 'comment-create' review.id %}"** method="post">
...
</form>
...
#views.py
...
class CommentCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
http_method_names = ['post']
model = Comment
form_class = CommentForm
#접근 제어
redirect_unauthenticated_users = True
raise_exception = confirmation_required_redirect
def form_valid(self, form):
form.instance.author = self.request.user
form.instance.review = Review.objects.get(id=self.kwargs.get('review_id'))
return super().form_valid(form)
def get_success_url(self):
return reverse('review-detail', kwargs={'review_id': self.kwargs.get('review_id')})
def test_func(self, user):
return EmailAddress.objects.filter(user=user, verified=True).exists()
만드는 view마다 접근제어 (mixin) 코드를 쓰는 대신에 mixins.py에 custom mixin 클래스 정의
3가지 접근 제어 조건
#mixins.py
from braces.views import LoginRequiredMixin, UserPassesTestMixin
from allauth.account.models import EmailAddress
from .functions import confirmation_required_redirect
class LoginAndVerificationRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
redirect_unauthenticated_users = True
raise_exception = confirmation_required_redirect
ef test_func(self, user):
return EmailAddress.objects.filter(user=user, verified=True).exists()
#views.py
...
class ReviewCreateView(LoginAndVerificationRequiredMixin, CreateView):
model = Review
form_class = ReviewForm
template_name = 'coplate/review_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
def get_success_url(self):
return reverse('review-detail', kwargs={'review_id': self.object.id})
class LoginAndOwnershipRequiredMixin(LoginRequiredMixin, UserPassesTestMixin)
redirect_unauthenticated_users = False
raise_exception = True
def test_func(self, user):
obj = self.get_object()
return obj.author == user
#urls.py
...
urlpatterns = [
...
path('comments/<int:comment_id>/edit/', views.CommentUpdateView.as_view(), name='comment-update'),
path('comments/<int:comment_id>/delete/', views.CommentDeleteView.as_view(), name='comment-delete'),
]
#views.py
class CommentUpdateView(LoginAndOwnershipRequiredMixin, UpdateView):
model = Comment
form_class = CommentForm
template_name = 'coplate/comment_update_form.html'
pk_url_kwarg = 'comment_id'
def get_success_url(self):
return reverse('review-detail', kwargs={'review_id': self.object.review.id})
class CommentDeleteView(LoginAndOwnershipRequiredMixin, DeleteView):
model = Comment
template_name = 'coplate/comment_confirm_delete.html'
pk_url_kwarg = 'comment_id'
def get_success_url(self):
return reverse('review-detail', kwargs={'review_id': self.object.review.id})
#review_detail.html
...
{% if user == comment.author %}
<div class="buttons">
<a href="**{% url 'comment-delete' comment.id %}**">삭제</a>
<span> | </span>
<a href="**{% url 'comment-update' comment.id %}**">수정</a>
</div>
{% endif %}
...
[리뷰(본문)의 좋아요]
#review_detail.html
...
**<form action="#" method="post">**
**{% csrf_token %}**
<button class="like-button" **type="submit"**>
<img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
<span> 좋아요 {{ review.likes.count }}</span>
</button>
**</form>**
...
#urls.py
...
urlpatterns = [
...
path('like/<int:content_type_id>/<int:object_id>/', views.ProcessLikeView.as_view(), name='process-like'),
]
#views.py
from django.contrib.contenttypes.models import ContentType
...
class ReviewDetailView(DetailView):
...
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = CommentForm()
**context['review_ctype_id'] = ContentType.objects.get(model='review').id**
return context
#review_detail.html
...
<div class="like-comment-header">
{% if user.is_authenticated %}
<form action="{% url 'process-like' review_ctype_id review.id%}" method="post">
{% csrf_token %}
<button class="like-button" type="submit">
<img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
<span> 좋아요 {{ review.likes.count }}</span>
</button>
</form>
{% else %}
<a class = "like-button" href="{% url 'account_login' %}?next={% url 'review_detail' review.id %}">
<img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
<span> 좋아요 {{ review.likes.count }}</span>
</a>
{% endif %}
<div class="comment-info">
<img src="{% static 'coplate/icons/ic-comment.svg' %}" alt="comment icon">
<span> 댓글 {{ review.comments.count }}</span>
</div>
</div>
...
request 파라미터
url 파라미터
request & kwargs 둘다 self를 통해 접근 가능
def get(self, request, *args, **kwargs):
user = request.user
review_id = kwargs.get('review_id')
# 또는
user = self.request.user
review_id = self.kwargs.get('review_id')
ProcessLikeView 정의하기
#views.py
class ProcessLikeView(LoginAndVerificationRequiredMixin, View):
http_method_names = ['post']
def post(self, request, *args, **kwargs):
like, created = Like.objects.get_or_create(
user = self.request.user,
content_type_id=self.kwargs.get('content_type_id'),
object_id=self.kwargs.get('object_id')
)
if not created:
like.delete()
return redirect(self.request.META['HTTP_REFEREER'])
유저가 리뷰에 좋아요를 눌렀는지 확인하는 방법
class ReviewDetailView(DetailView):
model = Review
template_name = 'coplate/review_detail.html'
pk_url_kwarg = 'review_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = CommentForm()
context['review_ctype_id'] = ContentType.objects.get(model='review').id
context['comment_ctype_id'] = ContentType.objects.get(model='comment').id
**user = self.request.user
if user.is_authenticated:
review = self.object
context['likes_review'] = Like.objects.filter(user=user, review=review).exists()**
return context
{% if likes_review %}
<img width="21px" src="{% static 'coplate/icons/ic-heart-orange.svg' %}" alt="filled like icon">
{% else %}
<img width="21px" src="{% static 'coplate/icons/ic-heart.svg' %}" alt="like icon">
{% endif %}
<span> 좋아요 {{ review.likes.count }}</span>
리뷰에 있는 코멘트 중에 유저가 좋아하는 애들만 필터해서 템플릿으로 넘겨줌
Comment.objects.filter(review=review).filter(likes__user=user)
views.py의 ReviewDetailView에 넘겨줌
#views.py
class ReviewDetailView(DetailView):
model = Review
template_name = 'coplate/review_detail.html'
pk_url_kwarg = 'review_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = CommentForm()
context['review_ctype_id'] = ContentType.objects.get(model='review').id
context['comment_ctype_id'] = ContentType.objects.get(model='comment').id
user = self.request.user
if user.is_authenticated:
review = self.object
context['likes_review'] = Like.objects.filter(user=user, review=review).exists()
**context['liked_comments'] = Comment.objects.filter(review=review).filter(likes__user=user)**
return context
class Like(models.Model):
...
class Meta:
unique_together = ['user', 'content_type', 'object_id']
특정 필드 값이 중복되면 안 되는 경우 unique=True
옵션
class MyModel(models.Model):
my_unique_field = models.CharField(unique=True)
여러 필드의 조합이 중복되면 안 되는 경우 unique_together
메타 옵션
class MyModel(models.Model):
my_field_a = models.CharField()
my_field_b = models.CharField()
class Meta:
unique_together = ['my_field_a', 'my_field_b']
#profile.html
<div class="follow-section">
<a href="#">
팔로워 {{ profile_user.followers.count }}
</a>
<span class="vert-divider">|</span>
<a href="#">
팔로잉 {{ profile_user.following.count }}
</a>
</div>
{% if user.is_authenticated and user != profile_user %}
<form class="follow-button" action="#" method="post">
{% csrf_token %}
<button class="cp-button small" type="submit">
팔로우
</button>
</form>
{% endif %}
#팔로우
user.following.add(profile_user)
user.following.add(profile_user_id)
#언팔로우
user.following.remove(profile_user)
user.following.remove(profile_user_id)
#urls.py
...
urlpatterns = [
path('users/<int:user_id>/follow/', views.ProcessFollowView.as_view(), name='process-follow'),
]
class ProcessFollowView(LoginAndVerificationRequiredMixin, View):
http_method_names = ['post']
def post(self, request, *args, **kwargs):
user = self.request.user
profile_user_id = self.kwargs.get('user_id')
if user.following.filter(id=profile_user_id).exists(): #현재 유저의 팔로잉 목록에 프로필 유저가 있는지
user.following.remove(profile_user_id)
else:
user.following.add(profile_user_id)
return redirect('profile', user_id=profile_user_id)
class ProfileView(DetailView):
model = User
template_name = 'coplate/profile.html'
pk_url_kwarg = 'user_id'
context_object_name = 'profile_user'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
**user = self.request.user
profile_user_id = self.kwargs.get('user_id')
if user.is_authenticated:
context['is_following'] = user.following.filter(id=profile_user_id).exists()**
context['user_reviews'] = Review.objects.filter(author__id=profile_user_id)[:4]
return context
#profile.html
{% if is_following %}
<button class="cp-button small secondary" type="submit">
언팔로우
</button>
{% else %}
<button class="cp-button small" type="submit">
팔로우
</button>
{% endif %}
#urls.py
...
urlpatterns = [
path('users/<int:user_id>/following/', views.FollowingListView.as_view(), name='following-list'),
path('users/<int:user_id>/followers/', views.FollowerListView.as_view(), name='follower-list'),
]
class FollowingListView(ListView):
model = User
template_name = 'coplate/following_list.html'
context_object_name = 'following'
paginate_by = 10
def get_queryset(self): #프로필유저가 팔로잉하는 유저 목록
profile_user = get_object_or_404(User, pk=self.kwargs.get('user_id'))
return profile_user.following.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile_user_id'] = self.kwargs.get('user_id')
return context
반복적으로 사용되는 템플릿 코드는 component html 파일로 만들어서 호출
{% include 'components/review_list.html' with reviews=latest_reviews empty_message="아직 리뷰가 없어요 :(" %}
{% include 'components/pagination.html' with page_obj=page_obj %}
class IndexView(View):
def get(self, request, *args, **kwargs):
context = {}
context['latest_reviews'] = Review.objects.all()[:4]
**user = self.request.user
if user.is_authenticated:
context['latest_following_reviews'] = Review.objects.filter(author__followers=user)[:4]**
return render(request, 'coplate/index.html', context)
{% if user.is_authenticated %}
<div class="header">
<h2>팔로잉 유저들의 리뷰</h2>
</div>
{% include 'components/review_list.html' with reviews=latest_following_reviews empty_message="아직 리뷰가 없어요 :(" %}
{% if latest_following_reviews %}
<a class="cp-button" href="{% url 'following-review-list' %}">팔로잉 유저 리뷰 모아보기</a>
{% endif %}
{% endif %}
#urls.py
...
urlpatterns = [
...
path('reviews/following/', views.FollowingReviewListView.as_view(), name='following-review-list'),
]
class FollowingReviewListView(LoginRequiredMixin, ListView):
model = Review
context_object_name = 'following_reviews'
template_name = 'coplate/following_review_list.html'
paginate_by = 8
def get_queryset(self):
return Review.objects.filter(author__followers=self.request.user)
{% extends "coplate_base/base_with_navbar.html" %}
{% block title %}팔로잉 유저들의 리뷰 ({{ paginator.count }}) | Coplate{% endblock title %}
{% block content %}
<main class="site-body">
<div class="content-list max-content-width">
<div class="header">
<h2>팔로잉 유저들의 리뷰 ({{ paginator.count }})</h2>
</div>
{% include 'components/review_list.html' with reviews=following_reviews empty_message="아직 리뷰가 없어요 :(" %}
{% if is_paginated %}
{% include 'components/pagination.html' with page_obj=page_obj %}
{% endif %}
</div>
</main>
{% endblock content %}
AND
<Model>.objects.filter(field1=val1).filter(field2=val2)
<Model>.objects.filter(field1=val1, filed2=val2)
OR: Q object 사용
from django.db.models import Q
q1 = Q(field1=val1)
q2 = Q(field2=val2)
<Model>.objects.filter(q1 | q2)
<Model>.objects.filter(Q(field1=val1) | Q(field2=val2))
리뷰 검색
Review.objects.filter(
Q(title__icontains=word)
| Q(restaurant_name__icontains=word)
| Q(content_icontains=word)
}
<div class="header-search">
<form class="search-form" action="#" method="get">
<input class="search-input" **name="query"** type="text" placeholder="식당, 음식 등을 검색해보세요" required>
<button class="cp-button search-button" type="submit">검색</button>
</form>
</div>
#urls.py
...
urlpatterns=[
...
path('search/', views.search_view, name='search'),
...
]
<form class="search-form" **action="{% url 'search' %}"** method="get">
<input class="search-input" name="query" type="text" placeholder="식당, 음식 등을 검색해보세요" required>
<button class="cp-button search-button" type="submit">검색</button>
</form>
from django.http import HttpResponse
...
def search_view(request):
query = request.GET.get('query', '')
return HttpResponse(f"검색어: {query}")
뷰를 클래스형 뷰로 전환
class SearchView(ListView):
model = Review
context_object_name = 'search_results'
template_name = 'coplate/search_results.html'
paginate_by = 8
def get_query(self):
query = request.GET.get('query', '')
return Review.objects.filter(
Q(title__icontains=query)
| Q(restaurant_name__icontains=query)
| Q(content__icontains=query)
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['query'] = self.request.GET.get('query', '')
return context
참고: 코드잇 장고 강의