본문 바로가기
카테고리 없음

[Django] - (5) 해시태그, 글 상세 페이지, 좋아요, 팔로워/팔로잉

by 16비트 2023. 5. 28.

해시태그

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 %}

 

접속테스트

 

 

댓글