728x90
- Terminal
- User 생성하기
python manage.py shell
from users.models import User
User.objects.filter(username="pystagram")
User.objects.filter(username="pystagram").exists()
User.objects.filter(username="no_user")
User.objects.filter(username="no_user").exists()
|
- users/views.py
from users.models import User
def signup(request):
if request.method == "POST":
form = SignupForm(data=request.POST, files=request.FILES)
if form.is_valid():
username = form.cleaned_data["username"]
password1 = form.cleaned_data["password1"]
password2 = form.cleaned_data["password2"]
prifile_image = form.cleaned_data["profile_image"]
short_description = form.cleaned_data["short_description"]
if password1 != password2:
form.add_error("password2", "비밀번호와 비밀번호 확인란의 값이 다릅니다.")
if User.objects.filter(username=username).exists():
form.add_error("username", "입력한 사용자명은 이미 사용중입니다.")
if form.errors:
context = {"form": form}
return render(request, "users/signup.html", context)
else:
user = User.objects.create_user(
username = username,
password = password1,
profile_image = profile_image,
short_description = short_description,
)
login(request, user)
return redirect("/posts/feeds/")
else:
form = SignupForm()
context = {"form": form}
return render(request, "users/signup.html", context)
|
4.6.4 SignupForm 내부에서 데이터 유효성 검사
- users/forms.py
- clean_username 메서드 작성
from django import forms
from django.core.exceptions import ValidationError
from users.models import User
class SignupForm(forms.Form):
...
def clean_username(self):
username = self.cleaned_data["username"]
if User.objects.filter(username=username).exists():
raise ValidationError(f"입력한 사용자명({username})은 이미 사용중입니다")
return username
|
- users/forms.py
- clean 메서드로 password1, password2 검증
class SignupForm(forms.Form):
def clean_username(self):
...
def clean(self):
password1 = self.cleaned_data["password1"]
password2 = self.cleaned_data["password2"]
if password1 != password2:
self.add_error("password2", "비밀번호와 비밀번호 확인란의 값이 다릅니다")
|
- users/views.py
- View 함수와 SignupForm 리팩토링
def signup(request):
if request.method == "POST":
form = SignupForm(data=request.POST, files=request.FILES)
if form.is_valid():
username = form.cleaned_data["username"]
password1 = form.cleaned_data["password1"]
prifile_image = form.cleaned_data["profile_image"]
short_description = form.cleaned_data["short_description"]
user = User.objects.create_user(
username = username
password = password1
prifile_image = prifile_image
short_description = short_description
)
login(request, user)
return redirect("/posts/feeds/")
else:
context = {"form": form}
return render(request, "users/signup.html", context)
else:
form = SignupForm()
context = {"form": form}
return render(request, "users/signup.html", context)
|
- users/forms.py
- View 함수와 SignupForm 리팩토링
class SignupForm(forms.Form):
def clean(self):
...
def save(self):
username = self.cleaned_data["username"]
password1 = self.cleaned_data["password1"]
profile_image = self.cleaned_data["profile_image"]
short_description = self.cleaned_data["short_description"]
user = User.objects.create_user(
username=username,
password=password1,
profile_image=profile_image,
short_description=short_description,
)
return user
|
- users/views.py
- SignupForm으로 이동시킨 save() 함수 삭제
- 새로 만든 save()함수를 사용하도록 변경
def signup(request):
if request.method == "POST":
form = SignupForm(data=request.POST, files=request.FILES)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("/posts/feeds/")
else:
context = {"form": form}
return render(request, "users/signup.html", context)
else:
form = SignupForm()
context = {"form": form}
return render(request, "users/signup.html", context)
|
- users/views.py
- 지속적으로 중복되어 나오는 로직 제거
def signup(request):
if request.method == "POST":
form = SignupForm(data=request.POST, files=request.FILES)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("/posts/feeds/")
else:
form = SignupForm()
context = {"form": form}
return render(request, "users/signup.html", context)
|
4.6.5 View 함수에서 진행되는 케이스
- GET 요청
- SignupForm()으로 생성된 빈 form을 사용자에게 보여줌
def signup(request):
if request.method == "POST":
# 해당 없음
else:
form = SignupForm()
# context에 빈 Form이 전달됨
context = {"form": form}
return render(request, "users/signup.html", context)
|
- POST 요청이며, 데이터를 받은 SignupForm이 유효한 경우
- SignupForm(data=...)으로 생성된 form의 save() 메서드로 User 생성, redirect로 경로가 변경됨
def signup(request):
# POST 요청 시 form이 유효하다면 최종적으로 rediret 처리됨
if request.method == "POST":
form = SignupForm(data=request.POST, files=request.FILES)
if form.is_valid():
user = form.save()
login(request, user)
return redirect("/posts/feeds/")
# 이후 로직은 실행되지 않음
|
- POST 요청이며, 데이터를 받은 SignupForm이 유효하지 않은 경우
- SignupForm(data=...)으로 생성된 form에는 error가 추가되며, 그 form을 사용자에게 보여줌
def signup(request):
if request.method == "POST":
form = SignupForm(data=request.POST, files=request.FILES)
if form.is_valid():
# 검증에 실패하여 이 영역으로 들어오지 못함
# context에 error를 포함한 form이 전달됨
context = {"form": form}
return render(request, "users/signup.html", context)
|
4.6.6 Template 스타일링과 구조 리팩토링
- templates/users/signup.html
{% load static %}
<!document html>
<html lang="ko">
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div id="signup">
<form method="POST" enctype="multipart/form-data">
<h1>Pystagram</h1>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-signup">가입</button>
</form>
</div>
</body>
</html>
|
- templates/base.html
- Template을 확장하는 {% extends %} 태그
{% load static %}
<!doctype html>
<html lang="ko">
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
|
- templates/users/login.html
{% extends 'base.html' %}
{% block content %}
<div id="login">
<form method="POST">
<h1>Pystagram</h1>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-login">로그인</button>
</form>
</div>
{% endblock %}
|
- templates/users/signup.html
{% extends 'base.html' %}
{% block content %}
<div id="signup">
<form method="POST" enctype="multipart/form-data">
<h1>Pystagram</h1>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-signup">가입</button>
</form>
</div>
{% endblock %}
|
- templates/users/signup.html
- 회원가입과 로그인 페이지 간의 링크 추가
<div id="signup">
<form method="POST" enctype="multipart/form-data">
...
<button type="submit" class="btn btn-signup">가입</button>
<a href="/users/login/">로그인 페이지로 이동</a>
</form>
</div>
|
- templates/users/login.html
<div id="login">
<form method="POST">
...
<button type="submit" class="btn btn-login">로그인</button>
<a href="/users/sighup/">회원가입 페이지로 이동</a>
</form>
</div>
|
Pystagram Project (2)
1. 피드 페이지
1.1 글/이미지/댓글 모델링
1.1.1 Model 구성
- posts/models.py
from django.db import models
class Post(models.Model):
user = models.ForeignKey("users.User", verbose_name="작성자", on_delete=models.CASCADE)
content = models.TextField("내용")
created = models.DateTimeField("생성일시", auto_now_add=True)
class PostImage(models.Model):
post = models.ForeignKey(Post, verbose_name="포스트", on_delete=models.CASCADE)
photo = models.ImageField("사진", upload_to="post")
class Comment(models.Model):
user = models.ForeignKey("users.User", verbose_name="작성자", on_delete=models.CASCADE)
post = models.ForeignKey(Post, verbose_name="포스트", on_delete=models.CASCADE)
content = models.TextField("내용")
created = models.DateTimeField("생성일시", auto_now_add=True)
|
- Terminal
python manage.py makemigrations
python manage.py migrate
|
1.1.2 admin 구현
- posts/admin.py
from django.contrib import admin
from posts.models import Post, PostImage, Comment
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = [
"id",
"content",
]
@admin.register(PostImage)
class PostImageAdmin(admin.ModelAdmin):
list_display = [
"id",
"post",
"photo",
]
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = [
"id",
"post",
"content",
]
|
1.2 admin에 연관 객체 표시
1.2.1 ForeignKey로 연결된 객체 확인
- posts/admin.py
class CommentInline(admin.TabularInline):
model = Comment
extra = 1
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
...
inlines = [
CommentInline,
]
|
- posts/admin.py
class CommentInline(admin.TabularInline):
...
class PostImageInline(admin.TabularInline):
model = PostImage
extra = 1
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
...
inlines = [
CommentInline,
PostImageInline,
]
|
1.2.2 썸네일 이미지 표시
- posts/admin.py
- 직접 admin을 조작해서 썸네일 표시 코드 추가
from django.contrib.admin.widgets import AdminFileWidget
from django.db import models
from django.utils.safestring import mark_safe
...
class CommentInline(admin.TabularInline):
...
# AdminFileWidget은 관리자 페이지에서 '파일 선택' 버튼을 보여주는 부분
# 이 widget을 커스텀하여 <img> 태그를 추가함
class InlineImageWidget(AdminFileWidget):
def render(self, name, value, attrs=None, renderer=None):
html = super().render(name, value, attrs, renderer)
if value and getattr(value, "url", None):
html = mark_safe(f'<img src="{value.url}" width="150" height="150">') + html
return html
# ImageField를 표시할 때, AdminFileWidget을 커스텀한 InlineImageWidget을 사용함
class PostImageInline(admin.TabularInline):
model = PostImage
extra = 1
formfield_overrides = {
models.ImageField: {
"widget": InlineImageWidget,
}
}
|
- Terminal
- 오픈소스 라이브러리를 사용한 썸네일 표시
pip install django-admin-thumbnails
|
- posts/admin.py
# 위에서 추가한 코드들은 모두 삭제하고 썸네일 라이브러리를 사용함
import admin_thumbnails
@admin_thumbnails.thumbnail("photo")
class PostImageInline(admin.TabularInline):
model = PostImage
extra = 1
|
1.3 피드 페이지
1.3.1 View 작성
- posts/views.py
from posts.models import Post
def feeds(request):
user = request.user
if not user.is_authenticated:
return redirect("/users/login/")
posts = Post.objects.all()
context = { "posts": posts }
return render(request, "posts/feeds.html", context)
|
1.3.2 작성자 정보 표시
- templates/posts/feeds.html
{% extends 'base.html' %}
{% block content %}
<nav>
<h1>Pystagram</h1>
</nav>
<div id="feeds" class="post-container">
{% for post in posts %}
<article class="post">
<header class="post-header">
{% if post.user.profile_image %}
<img src="{{ post.user.profile_image.url }}">
{% endif %}
<span>{{ post.user.username }}</span>
</header>
</article>
{% endfor %}
</div>
{% endblock %}
|
1.3.3 이미지 슬라이더 구현
- templates/base.html
- 이미지 슬라이드 자바스크립트, CSS 파일 불러오기
{% load static %}
<!doctype html>
<html lang="ko">
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<link rel="stylesheet" href="{% static 'splide/splide.css' %}">
<script src="{% static 'splide/splide.js' %}"></script>
</head>
<body>
...
|
- templates/posts/feeds.html
- Splide 라이브러리 사용
{% extends 'base.html' %}
{% block content %}
...
<div id="feeds" class="post-container">
{% for post in posts %}
<article class="post">
<header class="post-header">
...
</header>
<!-- 이미지 슬라이드 영역 시작 -->
<div class="post-images splide">
<div class="splide__track">
<ul class="splide__list">
{% for image in post.postimage_set.all %}
{% if image.photo %}
<li class="splide__slide">
<img src="{{ image.photo.url }}">
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
<!-- 이미지 슬라이드 영역 종료 -->
</article>
{% endfor %}
</div>
{% endblock %}
|
- templates/posts/feeds.html
- 템플릿 하단에 자바스크립트 코드 작성
{% block content %}
<div id="feeds" class="post-container">
...
</div>
<!-- content 블록의 최하단에 작성함 -->
<script>
const elms = document.getElementsByClassName('splide')
for (let i = 0; i < elms.length; i++){
new Splide(elms[i]).mount();
}
</script>
{% endblock %}
|
1.3.4 글 속성 출력
- templates/posts/feeds.html
- 글 내용 출력
<article class="post">
<header class="post-header">...</header>
<div class="post-images">...</div>
<div class="post-content">
{{ post.content|linebreaksbr }}
</div>
|
- templates/posts/feeds.html
- 좋아요/댓글 버튼 표시
<div class="post-content">...</div>
<div class="post-buttons">
<button>Likes(0)</button>
<span>Comments(0)</span>
</div>
|
- templates/posts/feeds.html
- 댓글 목록 표시
<div class="post-buttons">...</div>
<div class="post-comments">
<ul>
<1-- 각 Post에 연결된 PostComment들을 순회 -->
{% for comment in post.comment_set.all %}
<li>
<span>{{ comment.user.username }}</span>
<span>{{ comment.content }}</span>
</li>
{% endfor %}
</ul>
<button>Likes(0)</button>
<span>Comments(0)</span>
</div>
|
- templates/posts/feeds.html
- 작성일자, 댓글 입력창 표시
<div class="post-comments">...</div>
<small>{{ post.created }}</small>
<div class="post-comments-create">
<input type="text" placeholder="댓글 달기...">
<button type="submit">게시</button>
</div>
|
1.3.5 Template에 링크 추가
- templates/posts/feeds.html
- 메인 링크 추가
<nav>
<h1>
<a href="/posts/feeds/">Pystagram</a>
</h1>
</nav>
|
- templates/posts/feeds.html
- 로그아웃 버튼 추가
<nav>
<h1>
<a href="/posts/feeds/">Pystagram</a>
</h1>
<a href="/users/logout/">Logout</a>
</nav>
|
- 전체 코드 (feeds.html)
{% extends 'base.html' %}
{% block content %}
<nav>
<h1>
<a href="/posts/feeds/">Pystagram</a>
</h1>
<a href="/users/logout/">Logout</a>
</h1>
</nav>
<div id="feeds" class="post-container">
{% for post in posts %}
<article class="post">
<header class="post-header">
{% if post.user.profile_image %}
<img src="{{ post.user.profile_image.url }}">
{% endif %}
<span>{{ post.user.username }}</span>
</header>
<!-- 이미지 슬라이드 영역 시작 -->
<div class="post-images splide">
<div class="splide__track">
<ul class="splide__list">
{% for image in post.postimage_set.all %}
{% if image.photo %}
<li class="splide__slide">
<img src="{{ image.photo.url }}">
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
<!-- 이미지 슬라이드 영역 종료 -->
<div class="post-content">
{{ post.content|linebreaksbr }}
</div>
<div class="post-buttons">
<button>Likes(0)</button>
<span>Comments(0)</span>
</div>
<div class="post-comments">
<ul>
<!-- 각 Post에 연결된 PostComment들을 순회 -->
{% for comment in post.comment_set.all %}
<li>
<span>{{ comment.user.username }}</span>
<span>{{ comment.content }}</span>
</li>
{% endfor %}
</ul>
</div>
<small>{{ post.created }}</small>
<div class="post-comments-create">
<input type="text" placeholder="댓글 달기...">
<button type="submit">게시</button>
</div>
</article>
{% endfor %}
</div>
<!-- content 블록의 최하단에 작성함 -->
<script>
const elms = document.getElementsByClassName('splide')
for (let i = 0; i < elms.length; i++){
new Splide(elms[i]).mount();
}
</script>
{% endblock %}
|
2. 글과 댓글
2.1 댓글 작성
2.1.1 CommentForm 구현
- posts/forms.py
- ModelForm
from django import forms
from posts.models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = [
"content",
]
|
- Terminal
- 오류 발생
- posts_comment 테이블의 post_id 필드는 NULL을 허용하지 않는다는 메시지
- 오류 발생
python manage.py shell
from posts.forms import CommentForm
data = {"content": "SampleContent"}
form = CommentForm(data=data)
form.is_valid()
form.save()
|
- Terminal
- 오류 해결 방법
- CommentForm으로 Comment 객체를 일단 만들되, 메모리 상에 객체를 만들고 필요한 데이터를 나중에 채우기
- CommentForm에 NULL을 허용하지 않는 모든 필드를 선언하고 인스턴스 생성 시 유효한 데이터를 전달
- 첫 번째 방법으로 해결해보기
- 오류 해결 방법
python manage.py shell
from posts.forms import CommentForm
data = {"content": "SampleContent"}
form = CommentForm(data=data)
form.is_valid()
comment = form.save(commit=False)
print(comment.id)
from users.models import User
from posts.models import Post
user = User.objects.all()[0]
post = Post.objects.all()[0]
print(user)
print(post)
comment.user = user
comment.post = post
comment.save()
comment.id
|
728x90
'PYTHON-BACK' 카테고리의 다른 글
#파이썬 34일차_이커머스 클론코딩4_(2) (0) | 2024.08.21 |
---|---|
#파이썬 34일차_이커머스 클론코딩4_(1) (0) | 2024.08.21 |
#파이썬 32일차_이커머스 클론코딩2 (0) | 2024.08.19 |
#파이썬 31일차_이커머스 클론코딩1 (0) | 2024.08.16 |
#파이썬 31일차_게시판 만들기3 (0) | 2024.08.16 |