Веб-программирование → Мини соц сеть на Django. Часть 2.

В предыдущей статье мы создали основу для соцсети, где есть регистрация пользователей, заполнение дополнительных данных о пользователе и страница профиля.

В этой части мы добавим ленту событий, возможность добавлять посты, оставлять комментарии и лайкать посты.

Начнем с модели Поста.

Пост - это то, что выкладывает пользователь у себя на ленте. Это может быть просто текст, картинка или картинка с текстом. И так, у нашего поста будут поля: время создания, автор, текст, картинка. 

Откроем models.py и создадим там новую модель Post:

class Post(models.Model):
    datetime = models.DateTimeField(verbose_name=u"Дата", auto_now_add=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=u"Автор", related_name="posts")
    text = models.CharField(max_length=1000, verbose_name=u"Текст", null=True, blank=True)
    image = models.FileField(verbose_name=u"Картинка", null=True, blank=True)

    class Meta:
        ordering = ["-datetime"]

Кстати у ForeignKey есть свойство on_delete, которое говорит, как нужно реагировать если ссылаемый объект будет удален. Мы указали models.CASCADE - что означает каскадное удаление. В данном случае, если какой-то пользователь будет удален, то все его посты тоже будут удалены. ordering = ["-datetime"] - указывает что посты будут отсортированы по убыванию даты создания. 

Дальше создадим миграции и проведем их(makemigrations, migrate).

1. Лента

Нам нужно сделать так, чтобы на главной странице отображалась лента пользователя (timeline), если он залогинен. В ленте будут все посты. Главную страницу обслуживает класс HomeView, поэтому мы в нем добавим метод dispatch. И если пользователь уже залогинен, то будем рендерить шаблон для ленты (timeline.html), иначе шаблон главной страницы (home.html):

class HomeView(TemplateView):
    template_name = "home.html"
    timeline_template_name = "timeline.html"

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return render(request, self.template_name)

        context = {
            'posts': Post.objects.all()
        }
        return render(request, self.timeline_template_name, context)

А timeline.html вот такая:

{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-3">
                <div class="block left-menu">
                    <a href="{% url 'profile' %}">
                        <i class="fa fa-user-circle"></i> Мой профиль
                    </a>
                    <a href="{% url 'home' %}">
                        <i class="fa fa-newspaper-o"></i> Новости
                    </a>
                    <a href="{% url 'home' %}">
                        <i class="fa fa-users"></i> Мои друзья
                    </a>
                </div>
            </div>
            <div class="col-6 content">
                <div class="card">
                    <div class="card-body">
                        <form method="post" name="new-post-form" enctype="multipart/form-data">
                            {% csrf_token %}
                            <textarea class="form-control form-control-sm" type="text" name="text"
                                      placeholder="Что нового?"></textarea>
                            <label for="image">Прикрепить картинку:</label>
                            <input class="form-control form-control-sm" type="file" name="image"><br>
                            <input class="form-control btn btn-outline-success btn-sm" type="submit" value="Добавить">
                        </form>
                    </div>
                </div>
                <div class="timeline">
                    {% for post in posts %}
                        <div class="card">
                            <div class="card-body">
                                {% if post.image.name %}
                                    <img src="{{ post.image.url }}" class="img-thumbnail"><br>
                                {% endif %}
                                {{ post.text }}
                            </div>
                        </div>
                    {% endfor %}
                </div>
            </div>
            <div class="col-3">
                <div class="block" style="text-align: center;">
                    <b>{{ user.get_full_name }}</b>
                    {% if user.profile.avatar.name %}
                        <img src="{{ user.profile.avatar.url }}" class="mainpage-avatar-img img-thumbnail">
                    {% else %}
                        <img src="{% static 'img/user.jpg' %}" class="mainpage-avatar-img img-thumbnail">
                    {% endif %}
                    <div class="right-menu-links">
                        <a href="{% url 'profile' %}" class="btn btn-outline-primary btn-sm">
                            <i class="fa fa-user-circle"></i> Мой профиль
                        </a>
                        <a href="{% url 'edit_profile' %}" class="btn btn-outline-success btn-sm">
                            <i class="fa fa-pencil-square"></i> Редактировать
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

Это большой шаблон. Тут три блока: левый, правый и основной. Слева три кнопки. Справа информация о пользователе. В основном блоке есть форма добавления нового поста, и дальше перечислены все имеющиеся посты. 

Я тут добавил несколько стилей в файл styles.css:

.block{
    border: 1px solid #e2e2e2;
    border-radius: 4px;
    box-shadow: 0 0 5px -4px rgba(128, 128, 128, 0.69);
    background: white;
    padding: 10px;
    margin: 5px;
}

.left-menu a{
    display: block;
    line-height: 30px;
    color: rgb(20, 93, 148);
}

.mainpage-avatar-img{
    width: 100px;
    height: 100px;
    object-fit: cover;
}

.right-menu-links{
    margin-top: 20px;
}

.right-menu-links a{
    margin-top: 10px;
    margin-bottom: 5px;
}

.card{
    margin-bottom: 20px;
}

Если кто внимательно смотрел, то увидел вот такие штучки в html коде:

<i class="fa fa-user-circle"></i> 

Это иконки из CSS-библиотеки fontawesome. Чтобы их подключить, скачайте их архив. В архиве найдите папку fonts и скопируйте ее в папку static файлов проекта. Затем, найдите файл font-awesome.min.css. и скопируйте его в папку css вашего проекта. Потом этот css-файл нужно подключить в base.html:

<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">

После этого можете добавлять разные иконки в свой проект использую css-класс fa. Список иконок здесь. Например иконка карандашика и иконка Apple:

<i class="fa fa-pencil"></i>
<i class="fa fa-apple"></i>

В ленте у нас есть форма добавления нового поста. Давайте напишем обработчик для этой формы. Для начала, создадим джанго форму PostForm в файле forms.py:

class PostForm(forms.ModelForm):

    class Meta:
        model = Post
        exclude = ['author']

Импортируем Post из моделей.

Так, как добавления поста происходит на главной странице, мы добавим обработчик в класс HomeView. Теперь метод dispatch выглядит так:

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            return render(request, self.template_name)

        if request.method == 'POST':
            form = PostForm(request.POST, request.FILES)
            if form.is_valid():
                form.instance.author = request.user
                form.save()
                return redirect(reverse("home"))
        context = {
            'posts': Post.objects.all()
        }
        return render(request, self.timeline_template_name, context)

Используя PostForm мы получаем введенные пользователем данные и после проверки сохраняем форму, присвоив текущего пользователя как автора нового поста. 

Теперь, пользователи могут писать посты и видеть их. 

2. Комментарии

Комментарии будут связаны с постами. У комментария будет дата, связанный пост, текст и автор. Вот модель:

class Comment(models.Model):
    datetime = models.DateTimeField(verbose_name=u"Дата", auto_now_add=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=u"Автор", related_name="comments")
    post = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name=u"Пост", related_name="comments")
    text = models.CharField(max_length=1000, verbose_name=u"Текст", null=True, blank=True)

    class Meta:
        ordering = ["datetime"]

После этого создаем миграции и мигрируем(makemigrations, migrate).

Перед тем как начать оставлять комментарии. Мы немного поменяем внешний вид поста в ленте. Теперь <div id="timeline"> в файле timeline.html выглядит так:

<div class="timeline">
    {% for post in posts %}
        <div class="card">
            <div class="card-body post">
                <div class="post-title">
                    {% if post.author.profile.avatar.name %}
                        <img src="{{ post.author.profile.avatar.url }}" class="post-author-img img-thumbnail">
                    {% else %}
                        <img src="{% static 'img/user.jpg' %}" class="post-author-img img-thumbnail">
                    {% endif %}
                    <div class="post-author">
                        {{ post.author.get_full_name }}
                    </div>
                    <div class="post-datetime">
                        {{ post.datetime|date:"d M Y H:i" }}
                    </div>
                </div>
                {% if post.image.name %}
                    <img src="{{ post.image.url }}" class="img-thumbnail"><br>
                {% endif %}
                <div class="post-text">
                    {{ post.text|default_if_none:""|linebreaks|urlize }}
                </div>
            </div>
            <div class="card-footer">
                <div id="comments-list-post-{{ post.id }}">
                    {% for comment in post.comments.all %}
                        {% place_comment comment %}
                    {% endfor %}
                </div>
                <div class="comment-form">
                    {% if post.author.profile.avatar.name %}
                        <img src="{{ post.author.profile.avatar.url }}" class="post-author-img img-thumbnail">
                    {% else %}
                        <img src="{% static 'img/user.jpg' %}" class="post-author-img img-thumbnail">
                    {% endif %}
                    <div style="display: flex; margin-top: 4px;">
                        <input class="form-control form-control-sm comment-input"
                               placeholder="Оставить комментарий"  data-post-id="{{ post.id }}">
                    </div>
                </div>
            </div>
        </div>
    {% endfor %}
</div>

Тут есть аватарка, имя автора поста, время поста, комментарии и форма отправки комментария. Здесь для вывода всех комментариев мы написали:

{% for comment in post.comments.all %}
   {% place_comment comment %}
{% endfor %}

Мы берем все комментарии данного поста, и для каждого в цикле вызываем функцию place_comment. Такие функции в Джанго называются шаблонными тегами и филтрами (template tags). Наша функция place_comment здесь вставляет html код, для вывода комментария. Эту функцию мы опишем в специальном файле my_filters.py. В папке вашего приложения mysite создайте новый пакет templatetags (New->Python Package). Пакет это обычная папка, в котором есть пустой файл с названием __init__.py . В этой папке создайте файл  my_filters.py. И в этот файл впишите:

from django.template.loader_tags import register


@register.inclusion_tag("blocks/comment.html")
def place_comment(comment):
    return {'comment': comment}

 Эта функция является шаблонным тегом типа inclusion_tag. Она умеет рендерить указанный html-шаблон с параметрами. В данном случае шаблон находится в файле blocks/comment.html:

{% load staticfiles %}
<div class="comment">
    <div class="post-title">
        {% if comment.author.profile.avatar.name %}
            <img src="{{ comment.author.profile.avatar.url }}" class="post-author-img img-thumbnail">
        {% else %}
            <img src="{% static 'img/user.jpg' %}" class="post-author-img img-thumbnail">
        {% endif %}
        <div class="comment-body">
            <span class="comment-author">{{ comment.author.get_full_name }}</span> {{ comment.text|default_if_none:""|urlize }}
            <div class="post-datetime">
                {{ comment.datetime|date:"d M Y H:i" }}
            </div>
        </div>
    </div>
</div>

В этом шаблоне есть маленький блок для отображения комментария.

Чтобы функция place_comment сработало мы должны его подключить в шаблон. Для этого в файле timeline.html добавить сверху под {% extends %}:

{% load my_filters %}

После этого нужно перезагрузить сервер вручную. 

Если вы заметили я добавил очень много css-классов. Поэтому обновим наш файл styles.css следующими стилями:

.post{
    padding-bottom: 0;
}

.post-title{
    margin-bottom: 10px;
}

.post-author-img{
    width: 40px;
    height: 40px;
    object-fit: cover;
    float: left;
    margin-right: 10px;
}

.post-author{
    font-weight: bold;
    width: auto;
    overflow: hidden;
    height: 20px;
    font-size: 14px;
    display: block;
}

.post-datetime{
    display: block;
    width: auto;
    overflow: hidden;
    height: 20px;
    font-size: 11px;
    color: grey;
}

.post-text{
    color: #414142;
    font-size: 14px;
}

.comment{
    color: #414142;
    font-size: 14px;
}

.comment-body{
    overflow: hidden;
}

.comment-author{
    font-weight:bold;
    color: rgb(68, 91, 139);
}

Отображать комментарии мы умеем. Но их все равно нет. Давайте теперь дадим пользователям возможность добавлять комментарии. 

Мы будем добавлять комментарии асинхронно, т.е. не перезагружая страницу с помощью ajax. Будем отправлять ajax запрос на  url /post-comment/ в ответ будем получать кусок html-кода, который будем добавлять в конец списка комментариев. 

Для начала давайте создадим view для обработки добавления комментария. Откройте views.py  и создайте класс PostCommentView:

class PostCommentView(View):
    def dispatch(self, request, *args, **kwargs):
        post_id = request.GET.get("post_id")
        comment = request.GET.get("comment")
        if comment and post_id:
            post = Post.objects.get(pk=post_id)
            comment = Comment(text=comment, post=post, author=request.user)
            comment.save()
            return render(request, "blocks/comment.html", {'comment': comment})
        return HttpResponse(status=500, content="")

Эта вьюшка принимает по GET запросу два параметра post_id и comment. Если они правильно переданы, то создаем новый объект Comment и сохраняет. Возвращает срендеренный шаблон comment.html, который мы использовали для отображения комментария. Здесь тоже возвращаем этот блок с новым комментарием. 

А если параметры заполнены неправильно, то возвращает HTTP ответ с кодом 500, это код ошибки. 

Теперь нужно в urls.py добавить url для этого view:

url(r'^post-comment/$', PostCommentView.as_view()),

Теперь добавим обработку отправки сообщения на фронтенде. Для этого откройте scripts.js и добавьте функцию initCommenting() чтобы было так:

function initBootstrapForms() {
    $("form.bootstrap-form").find("input,textarea").addClass("form-control");
    $("form.bootstrap-form").find("input[type='submit']").removeClass("form-control");
}

function initCommenting(){
    $(".comment-input").on("keyup", function(event){
        var input = $(this);
        if(event.keyCode === 13) {
            var comment = $(this).val().trim();
            var post_id = $(this).data("post-id");
            if (comment.length > 0) {
                $.ajax("/post-comment/", {
                    data: {
                        post_id: post_id,
                        comment: comment
                    },
                    success: function(html){
                        $("#comments-list-post-"+post_id).append(html);
                        $(input).val("");
                    }
                })
            }
            return false;
        }
    })
}

$(document).ready(function(){
    initBootstrapForms();
    initCommenting();
});

В этой функции мы берем элементы с классом .comment-input (поля ввода комментария) и вешаем обработчик на событие keyup (клавише отпущена). Если отпущенная клавиша была Enter (13), то берем введенный текст комментария, получаем id поста и отправляем запрос на url /post-comment/. Если запрос выполнене успешно(success), то мы в ответ получаем кусок html c комментарием. Этот кусок добавляем в конец блока с комментариями данного поста и очищаем поле ввода. 

Если вы внимательно посмотрите код timeline.html в поле ввода комментарий мы указываем data-post-id={{ post.id }}. Этот атрибут будет указывать какому посту будет принадлежать введенный комментарий. Так как на странице будет много постов, без этого атрибута мы не можем узнать какому же посту был добавлен комментарий. 

Значение атрибута data-post-id мы можем получить с помощью jQuery $(this).data("post-id") что и делаем выше в js коде. вместо this может быть любоей html элемент у которого есть такой атрибут. 

Теперь перезагрузите сервер и проверьте работу комментариев и постов. 

Исходный код проекта лежит здесь: https://gitlab.com/masteraalish/django-social-network-example

4138 0
Alisher Alikulov