해시태그
posts/models.py
(venv) PS C:\myproject\pystagram> python manage.py makemigrations
(venv) PS C:\myproject\pystagram> python manage.py migrate
다대다 모델 admin
posts/admin.py
from posts.models import Post, PostImage, Comment, HashTag
@admin.register(HashTag)
class HashTagAdmin(admin.ModelAdmin):
pass
접속테스트
태그에 이름이 나오게하기
posts/models.py
Post생성시 체크박스로 태그 선택할 수 있게 하기
posts/admins.py
from django.db.models import ManyToManyField
from django.forms import CheckboxSelectMultiple
접속테스트
Post에 해시태그 표시
templates/posts/feeds.html
<div class="post-tags">
{% for tag in post.tags.all %}
<span>#{{ tag.name }}</span>
{% endfor %}
</div>
접속테스트
해시태그 검색 - 기본구조
posts/views.py
posts/urls.py
templates/posts/tags.html (새로 생성)
{% extends 'base.html' %}
{% block content %}
<nav>
<h1>
<a href="{% url 'posts:feeds' %}">Pystagram</a>
</h1>
<a href="{% url 'posts:post_add' %}">Add post</a>
<a href="{% url 'users:logout' %}">Logout</a>
</nav>
<div id="tags">
<header class="tags-header">
<h2>#{{ tag_name }}</h2>
<div>게시물 {{ posts.count }}</div>
</header>
<div class="post-grid-container">
<div class="post-grid"></div>
<div class="post-grid"></div>
<div class="post-grid"></div>
<div class="post-grid"></div>
<div class="post-grid"></div>
<div class="post-grid"></div>
<div class="post-grid"></div>
<div class="post-grid"></div>
</div>
</div>
{% endblock %}
접속테스트
view에서 해시태그 찾고, 해당하는 Post 목록 돌려주기
posts/views.py
from posts.models import Post, Comment, PostImage, HashTag
def tags(request, tag_name):
tag = HashTag.objects.get(name=tag_name)
posts = Post.objects.filter(tags=tag)
context = {
"tag_name": tag_name,
"posts": posts,
}
return render(request, 'posts/tags.html', context)
Post목록 만큼 Grid 랜더링, tag_name을 html에 표시하기
templates/posts/tags.html
접속테스트
각각의 Post가 가진 첫 번째 이미지 보여주기
접속테스트
없는 해시태그로 검색했을 때 처리 - 현재 없는 태그 검색시 페이지 에러가 난다
posts/views.py
templates/posts/tags.html
접속테스트
피드 페이지의 글에서 해시태그 링크 생성
templates/posts/feeds.html
접속테스트
글 작성페이지에서 해시태그 쓰기 추가
templates/post_add.html
posts/views.py
tag_string = request.POST.get("tags")
if tag_string:
tag_names = [tag_name.strip() for tag_name in tag_string.split(",")]
for tag_name in tag_names:
tag, _ = HashTag.objects.get_or_create(name=tag_name)
post.tags.add(tag)
접속테스트
글 상세 페이지
posts/views.py
def post_detail(request, post_id):
post = Post.objects.get(id=post_id)
comment_form = CommentForm()
context = {
"post": post,
"comment_form": comment_form,
}
return render(request, "posts/post_detail.html", context)
posts/urls.py
templates/posts/post_detail.html (새로 생성)
접속테스트
templates/posts/feeds.html를 전체 복사해서 {% for %}문 지우고 붙여넣기
접속테스트
{% include %} 태그로 Template 재사용
templates/posts/post.html (새로 생성)
하나의 글을 나타내는 <article>...</article> 붙여넣기
templates/posts/feeds.html에 삽입
templates/posts/post_detail.html에 삽입
<nav>태그의 내용을 별도의 nav.html로 이동
templates/nav.html (새로 생성)
templates/posts/feeds.html 수정
templates/post/post_detail.html 수정
해시태그 검색 결과에 링크 추가
feeds.html에서 게시물 해시태그 클릭시 검색결과를 모아줌. 반대로 해시태그로 검색된 결과를 누를시 해당 게시물 상세페이지로 이동하는 링크 추가
templates/posts/tags.html
접속테스트
ㅅㅄㅄㅄㅄㅄㅄㅄㅄㅄㅂ
글 작성 후 이동할 위치 지정
현재 상세페이지에서 댓글을 작성하던 feed.html에서 댓글을 작성하던 다 feeds.html로 이동하게 된다.
이동시킬 페이지를 다르게 지정하기
templates/posts/post_detail.html
templates/posts/post.html
posts/views.py
templates/posts/feeds.html
{% for post in posts %}
{% with post.id|stringformat:"s" as post_id %}
{% url 'posts:feeds' as action_redirect_to %}
{% include 'posts/post.html' with action_redirect_url=action_redirect_to|add:'#post-'|add:post_id %}
{% endwith %}
접속테스트
Custom template filter
post/tempatetags/__init__.py와 custom_tags.py (새로 생성)
from django import template
register = template.Library()
@register.filter
def concat(value, arg):
return f"{value}{arg}"
templates/posts/feeds.html
{% load custom_tags %}
{% url 'posts:feeds' as action_redirect_to %}
{% include 'posts/post.html' with action_redirect_url=action_redirect_to|concat:'#post-'|concat:post_id %}
Template 중복 코드 제거
base.html 분할
templates/_base.html (새로 생성)
{% load static %}
<!doctype html>
<html lang="ko">
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<title>Pystagram</title>
{% block head %}{% endblock %}
</head>
<body>
{% block base_content %}{% endblock %}
</body>
</html>
로그인, 회원가입에서 사용할 base.html
{% extends '_base.html' %}
{% block base_content %}
{% block content %}{% endblock %}
{% endblock %}
글 작성에서 사용할 base_nav.html
{% extends '_base.html' %}
{% block base_content %}
{% include 'nav.html' %}
{% block content %}{% endblock %}
{% endblock %}
피드, 글 상세에서 사용할 base_slider.html
{% extends '_base.html' %}
{% load static %}
{% block head %}
<link href="{% static 'splide/splide.css' %}" rel="stylesheet">
<script src="{% static 'splide/splide.js' %}"></script>
{% endblock %}
{% block base_content %}
{% include 'nav.html' %}
{% block content %}{% endblock %}
<script>
const elms = document.getElementsByClassName('splide');
for (let i = 0; i < elms.length; i++) {
new Splide(elms[i]).mount();
}
</script>
{% endblock %}
분할한 Template을 사용하도록 코드 수정
base_slider.html을 사용하는 페이지. templates/posts/feeds.html, post_detail.html
{% extends 'base_slider.html' %}
{% load custom_tags %}
{% block content %}
<div id="feeds" class="post-container">
{% for post in posts %}
{% url 'posts:feeds' as action_redirect_to %}
{% include 'posts/post.html' with action_redirect_url=action_redirect_to|concat:'#post-'|concat:post.id %}
{% endfor %}
</div>
{% endblock %}
{% extends 'base_slider.html' %}
{% block content %}
<div id="feeds" class="post-container">
{% url 'posts:post_detail' post.id as action_redirect_to %}
{% include 'posts/post.html' with action_redirect_url=action_redirect_to %}
</div>
{% endblock %}
base_nav.html을 사용하는 페이지. templates/posts/tags.html, post_add.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="tags">
<header class="tags-header">
<h2>#{{ tag_name }}</h2>
<div>게시물 {{ posts.count }}</div>
</header>
<div class="post-grid-container">
{% for post in posts %}
{# Post에 연결된 PostImage가 있으며, 연결된 첫 번째 PostImage의 photo가 비어있지 않은 경우 #}
{% if post.postimage_set.first and post.postimage_set.first.photo %}
<div class="post-grid">
<a href="{% url 'posts:post_detail' post_id=post.id %}">
<img src="{{ post.postimage_set.first.photo.url }}" alt="">
</a>
</div>
{% endif %}
{% empty %}
<p>검색된 게시물이 없습니다</p>
{% endfor %}
</div>
</div>
{% endblock %}
{% extends 'base_nav.html' %}
{% block content %}
<div id="post-add">
<h1>Post 작성</h1>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div>
<!-- label의 for속성에는 가리키는 input의 id값을 입력 -->
<label for="id_images">이미지</label>
<input id="id_images" name="images" type="file" multiple>
</div>
{{ form.as_p }}
<div>
<label for="id_tags">해시태그</label>
<input id="id_tags" name="tags" type="text" placeholder="쉼표(,)로 구분하여 여러 태그 입력">
</div>
<button type="submit">게시</button>
</form>
</div>
{% endblock %}
좋아요
users/models.py
like_posts = models.ManyToManyField(
"posts.Post",
verbose_name="좋아요 누른 Post 목록",
related_name="like_users",
blank=True,
)
(venv) PS C:\myproject\pystagram> python manage.py makemigrations
(venv) PS C:\myproject\pystagram> python manage.py migrate
users/admin.py
접속테스트
posts/models.py
users/model.py
접속테스트
PostAdmin에 like_users 추가
posts/admin.py
class LikeUserInline(admin.TabularInline):
model = Post.like_users.through
verbose_name = "좋아요 한 User"
verbose_name_plural = f"{verbose_name} 목록"
extra = 1
def has_change_permission(self, request, obj=None):
return False
접속테스트
좋아요 토글 액션
posts/views.py
def post_like(request, post_id):
post = Post.objects.get(id=post_id)
user = request.user
# 사용자가 "좋아요를 누른 Post목록"에 "좋아요 버튼을 누른 Post"가 존재한다면
if user.like_posts.filter(id=post.id).exists():
# 좋아요 목록에서 삭제한다
user.like_posts.remove(post)
# 존재하지 않는다면 좋아요 목록에 추가한다.
else:
user.like_posts.add(post)
# next로 값이 전달되었다면 해당 위치로, 전달되지 않았다면 피드페이지에서 해당 Post위치로 이동한다
url_next = request.GET.get("next") or reverse("posts:feeds") + f"#post-{post.id}"
return HttpResponseRedirect(url_next)
posts/urls.py
templates/posts/post.html
좋아요 버튼 form추가
<form action="{% url 'posts:post_like' post_id=post.id %}?next={{ action_redirect_url }}" method="POST">
{% csrf_token %}
<button type="submit"
{% if user in post.like_users.all %}
style="color: red;"
{% endif %}>
Likes({{ post.like_users.count }})
</button>
</form>
<span>Comments({{ post.comment_set.count }})</span>
접속테스트 - 두 번 누르면 취소도 가능
팔로워/팔로잉
users/models.py
class Relationship(models.Model):
from_user = models.ForeignKey(
"users.User",
verbose_name="팔로우를 요청한 사용자",
related_name="following_relationships",
on_delete=models.CASCADE,
)
to_user = models.ForeignKey(
"users.User",
verbose_name="팔로우 요청의 대상",
related_name="follower_relationships",
on_delete=models.CASCADE,
)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"관계 ({self.from_user} -> {self.to_user})"
users/models.py
following = models.ManyToManyField(
"self",
verbose_name="팔로우 중인 사용자들",
related_name="followers",
symmetrical=False,
through="users.Relationship",
)
(venv) PS C:\myproject\pystagram> python manage.py makemigrations
(venv) PS C:\myproject\pystagram> python manage.py migrate
users/admin.py
class FollowersInline(admin.TabularInline):
model = User.following.through
fk_name = "from_user"
verbose_name = "내가 팔로우 하고 있는 사용자"
verbose_name_plural = f"{verbose_name} 목록"
extra = 1
class FollowingInline(admin.TabularInline):
model = User.following.through
fk_name = "to_user"
verbose_name = "나를 팔로우 하고 있는 사용자"
verbose_name_plural = f"{verbose_name} 목록"
extra = 1
users/admin.py
접속테스트
프로필 페이지 - 기본구조
users/views.py
def profile(request, user_id):
return render(request, "users/profile.html")
users/urls.py
templates/users/profile.html (새로 생성)
{% extends 'base_nav.html' %}
{% block content %}
<div id="profile">
<h1>Profile</h1>
</div>
{% endblock %}
피드페이지에서 프로필을 클릭하면 프로필 페이지로의 링크 추가
templates/posts/post.html
접속테스트
프로필 Template에 정보 전달
users/views.py
from django.shortcuts import render, redirect, get_object_or_404
from users.models import User
def profile(request, user_id):
user = get_object_or_404(User, id=user_id)
context = {
"user": user,
}
return render(request, "users/profile.html", context)
templates/users/profile.html
{% extends 'base_nav.html' %}
{% block content %}
<div id="profile">
<div class="info">
<!-- 프로필 이미지 영역 -->
{% if user.profile_image %}
<img src="{{ user.profile_image.url }}">
{% endif %}
<!-- 사용자 정보 영역 -->
<div class="info-texts">
<h1>{{ user.username }}</h1>
<div class="counts">
<dl>
<dt>Posts</dt>
<dd>{{ user.post_set.count }}</dd>
<dt>Followers</dt>
<dd>{{ user.followers.count }}</dd>
<dt>Following</dt>
<dd>{{ user.following.count }}</dd>
</dl>
</div>
<p>{{ user.short_description }}</p>
</div>
</div>
<!-- 사용자가 작성한 Post목록 -->
<div class="post-grid-container">
{% for post in user.post_set.all %}
{% if post.postimage_set.first %}
{% if post.postimage_set.first.photo %}
<div class="post-grid">
<a href="{% url 'posts:post_detail' post_id=post.id %}">
<img src="{{ post.postimage_set.first.photo.url }}" alt="">
</a>
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
접속테스트
base_profile.html 구성
templates/base_profile.html (새로 생성)
{% extends 'base_nav.html' %}
{% block content %}
<div id="profile">
<div class="info">
<!-- 프로필 이미지 영역 -->
{% if user.profile_image %}
<img src="{{ user.profile_image.url }}">
{% endif %}
<!-- 사용자 정보 영역 -->
<div class="info-texts">
<h1>{{ user.username }}</h1>
<div class="counts">
<dl>
<dt>Posts</dt>
<dd>{{ user.post_set.count }}</dd>
<dt>Followers</dt>
<dd>{{ user.followers.count }}</dd>
<dt>Following</dt>
<dd>{{ user.following.count }}</dd>
</dl>
</div>
<p>{{ user.short_description }}</p>
</div>
</div>
{% block bottom_data %}{% endblock %}
</div>
{% endblock %}
templates/users/profile.html
{% extends 'base_profile.html' %}
{% block bottom_data %}
<!-- 사용자가 작성한 Post목록 -->
<div class="post-grid-container">
{% for post in user.post_set.all %}
{% if post.postimage_set.first %}
{% if post.postimage_set.first.photo %}
<div class="post-grid">
<a href="{% url 'posts:post_detail' post_id=post.id %}">
<img src="{{ post.postimage_set.first.photo.url }}" alt="">
</a>
</div>
{% endif %}
{% endif %}
{% endfor %}
</div></div>
{% endblock %}
접속테스트 - 상속되어서 페이지가 잘 뜬다
팔로우/팔로잉 목록
users/views.py
def followers(request, user_id):
user = get_object_or_404(User, id=user_id)
context = {
"user": user,
"title": "Followers",
"relationships": user.follower_relationships.all(),
}
return render(request, "users/followers.html", context)
def following(request, user_id):
user = get_object_or_404(User, id=user_id)
context = {
"user": user,
"title": "Following",
"relationships": user.following_relationships.all(),
}
return render(request, "users/following.html", context)
users/urls.py
templates/users/followers.html
{% extends 'base_profile.html' %}
{% block bottom_data %}
<div class="relationships">
<h3>Followers</h3>
{% for relationship in relationships %}
<div class="relationship">
<a href="{% url 'users:profile' user_id=relationship.from_user.id %}">
{% if relationship.from_user.profile_image %}
<img src="{{ relationship.from_user.profile_image.url }}">
{% endif %}
<div class="relationship-info">
<span>{{ relationship.from_user.username }}</span>
<span>{{ relationship.created|date:"y.m.d" }}</span>
</div>
</a>
</div>
{% endfor %}
</div>
{% endblock %}
templates/users/following.html
{% extends 'base_profile.html' %}
{% block bottom_data %}
<div class="relationships">
<h3>Following</h3>
{% for relationship in relationships %}
<div class="relationship">
<a href="{% url 'users:profile' user_id=relationship.to_user.id %}">
{% if relationship.to_user.profile_image %}
<img src="{{ relationship.to_user.profile_image.url }}">
{% endif %}
<div class="relationship-info">
<span>{{ relationship.to_user.username }}</span>
<span>{{ relationship.created|date:"y.m.d" }}</span>
</div>
</a>
</div>
{% endfor %}
</div>
{% endblock %}
templates/base_profile.html
접속테스트
팔로우 버튼
users/views.py
from django.http import HttpResponseRedirect
from django.urls import reverse
def follow(request, user_id):
# 로그인 한 유저
user = request.user
# 팔로우 하려는 유저
target_user = get_object_or_404(User, id=user_id)
# 팔로우 하려는 유저가 이미 자신의 팔로잉 목록에 있는 경우
if target_user in user.following.all():
# 팔로잉 목록에서 제거
user.following.remove(target_user)
# 팔로우 하려는 유저가 자신의 팔로잉 목록에 없는 경우
else:
# 팔로잉 목록에 추가
user.following.add(target_user)
# 팔로우 토글 후 이동할 URL이 전달되었다면 해당 주소로,
# 전달되지 않았다면 로그인 한 유저의 프로필 페이지로 이동
url_next = request.GET.get("next") or reverse("users:profile", args=[user.id])
return HttpResponseRedirect(url_next)
users/urls.py
templates/posts/post.html
{% if user != post.user %}
<form action="{% url 'users:follow' user_id=post.user.id %}?next={{ action_redirect_url }}" method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
<!-- 이 Post의 작성자가 이미 자신의 팔로잉 목록에 포함된 경우 -->
{% if post.user in user.following.all %}
Unfollow
<!-- 이 Post의 작성자를 아직 팔로잉 하지 않은 경우 -->
{% else %}
Follow
{% endif %}
</button>
</form>
{% endif %}
접속테스트
댓글