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

Эта статья предполгает, что вы уже прочитали все предыдущие статьи про Django и освоили все, что там написано. 

Обещал всем показать как можно написать мини соц сеть на Джанго. Придется выполнить обещанное. Надеюсь статья станет полезным и входновит кого-то создать соцсеть намного лучше. 

Какой функционал будет в нашей мини соцсети:

 


1. Регистрация пользователей, все желающие могут регистрироваться.
2. Личные данные пользователя: аватарка, пол, ДР, родной город, статус отношений, краткая инфо о себе.
3. Пользователи могут писать посты c текстом и картинкой. 
4. Есть поиск других пользователей по разным параметрам.
5. Пользователи могут "дружить" с другими пользователями. Сначала первый отправляет запрос, потом второй принимает. 
6. Пользователи на своей главной странице будут видеть посты своих друзей вместе со своими. 
7. Пользователи могут "лайкать" посты друг друга
8. И так оставлять комментарии под ними. 
 

1. Скелет проекта

Будем использовать Bootstrap 4 чтобы много стилей не писать. Создадим пустой Django проект. Настроим статические файлы. Создадим главный html-шаблон, который будет выглядеть примерно так:

base.html

{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Мини Соцсеть</title>
    <link href="{% static 'css/bootstrap-4/bootstrap.css' %}" rel="stylesheet">
    <link href="{% static 'css/bootstrap-4/bootstrap-grid.css' %}" rel="stylesheet">
    <link href="{% static 'css/bootstrap-4/bootstrap-reboot.css' %}" rel="stylesheet">
    <link href="{% static 'css/styles.css' %}" rel="stylesheet">
    <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
    <script src="{% static 'js/bootstrap-4/popper.min.js' %}"></script>
    <script src="{% static 'js/bootstrap-4/bootstrap.min.js' %}"></script>
    <script src="{% static 'js/scripts.js' %}"></script>
</head>
<body>
    {% include 'blocks/navbar.html' %}
    {% block content %}
    {% endblock %}
</body>
</html>

Тут видно какие статик файлы я подключил. styles.css и scripts.js - это пока пустые файлы, которые мы будем далее заполнять. Есть один блок content, в котором будет основная часть страницы. Внутри body мы включаем другой шаблон для навигационного бара. Этот шаблон будет лежать в папке templates/blocks в файле navbar.html:

<nav class="navbar navbar-expand-lg navbar-light justify-content-between" style="background-color: #9fcdff;">
    <a class="navbar-brand" href="/">Мини Соцсеть</a>
    <form class="form-inline">
        <input class="form-control form-control-sm mr-sm-2" type="search" placeholder="Поиск" aria-label="Search">
        <button class="btn btn-sm btn-outline-primary my-2 my-sm-0">Поиск</button>
    </form>
</nav>

Это код бутстраповского навбара, скопирован с сайта Bootstrap. Так как мы подключили навбар в файле base.html, то он будет теперь во всех страницах. 

А главная страница будет такой:

home.html

{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
    <div class="container content">
        <h2 class="text-center">Добро пожаловать в мини Соцсеть!</h2>
        <div class="text-center">
            <img src="{% static 'img/network.png' %}">
            <br>
            <br>
            <br>
            <a href="#" class="btn btn-info">Войти</a>
            <a href="#" class="btn btn-primary">Регистрация</a>
        </div>
    </div>
{% endblock %}

Добавим View для главной страницы

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

И url:


from mysite.views import HomeView

urlpatterns = [
    url(r'^$', HomeView.as_view(), name="home"),
    url(r'^admin/', admin.site.urls),
] + static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS)

Я добавил там картинку:

и стили в файл styles.css:

body{
    background-color: #efefef;
}

.content{
    border: 1px solid #e2e2e2;
    border-top: none;
    border-radius: 0 0 4px 4px;
    box-shadow: 0 0 5px -4px rgba(128, 128, 128, 0.69);
    background: white;
    min-height: 500px;
    padding-top: 20px;
    padding-bottom: 20px;
}

2. Регистрация пользователей

При регистрации человек будет вводить как обычно email, имя и пароль. После регистрации он должен ввести свои личные данные. Модель User из комплекта Django содержит такие поля как email, first_name, last_name и пароль. Поэтому чтобы хранить остальные личные данные пользователя, мы создадим другую модель Profile, который будет иметь связь с моделью User один-к-одному.

Опишем модель Profile в файле models.py:

# coding=utf-8
from django.contrib.auth.models import User
from django.db import models

GENDER_CHOICES = [
    ['male', u"Мужской"],
    ['female', u"Женский"],
]

REL_CHOICES = [
    ['none', u"Не определенно"],
    ['single', u"Холост"],
    ['in_a_rel', u"В отношениях"],
    ['engaged', u"Помолвлен(а)"],
    ['married', u"Женат/Замужем"],
    ['in_love', u"Влюблен(а)"],
    ['complicated', u"Все сложно"],
]


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=u"Пользователь")
    avatar = models.FileField(verbose_name=u"Аватар", null=True, blank=True)
    bio = models.TextField(max_length=500, blank=True, null=True, verbose_name=u"О себе")
    city = models.CharField(max_length=30, blank=True, null=True, verbose_name=u"Город")
    birth_date = models.DateField(null=True, blank=True, verbose_name=u"Дата рождения")
    gender = models.CharField(max_length=10, verbose_name=u"Пол", choices=GENDER_CHOICES, default="male")
    relationship = models.CharField(max_length=20, verbose_name=u"Статус отношений", choices=REL_CHOICES, default="none")

Для двух полей мы указали какие варианты там могут быть(choices). В choices мы указываем список значений которые может принимать данное поле. В каждом значении по две части: первое для хранения в БД('single', 'male', ..), а второе для отображения ("Помолвлена", ,..). 

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

Дальше создаем страницу регистрации, логина и профиля. 

Сначала добавим RegisterView во views.py:


class RegisterView(TemplateView):
    template_name = "registration/register.html"

    def dispatch(self, request, *args, **kwargs):
        form = RegisterForm()
        if request.method == 'POST':
            form = RegisterForm(request.POST)
            if form.is_valid():
                self.create_new_user(form)
                messages.success(request, u"Вы успешно зарегистрировались!")
                return redirect("/")

        context = {
            'form': form
        }
        return render(request, self.template_name, context)

    def create_new_user(self, form):
        email = None
        if 'email' in form.cleaned_data:
            email = form.cleaned_data['email']
        User.objects.create_user(form.cleaned_data['username'], email, form.cleaned_data['password'],
                                 first_name=form.cleaned_data['first_name'],
                                 last_name=form.cleaned_data['last_name'])

Класс RegisterView будет обрабатывать страницу регистрации. Тут мы создаем джанго форму RegisterForm. Такие формы сами показывают на страницу html форму c полями ввода и умеют проверять правильность заполнения. Если форма правильно заполнена, то мы создаем нового пользователя и переносим пользователя на главную страницу. 

RegisterForm  мы опишем в файле forms.py:

# coding=utf-8
from django import forms


class RegisterForm(forms.Form):
    username = forms.CharField(label=u"Имя пользователя")
    first_name = forms.CharField(label=u"Имя")
    last_name = forms.CharField(label=u"Фамилия")
    email = forms.EmailField(label=u"Email", required=False)
    password = forms.CharField(label=u"Пароль", widget=forms.PasswordInput)
    password_confirm = forms.CharField(label=u"Подтвердите пароль", widget=forms.PasswordInput)

    def is_valid(self):
        valid = super(RegisterForm, self).is_valid()
        if self.cleaned_data['password'] != self.cleaned_data['password_confirm']:
            self.add_error("password_confirm", u"Пароли не совпадают")
            return False
        return valid

Джанго-формы похожи на модели, тут описываются какие поля будут в форме. Метод is_valid() проверяет правильно ли заполнена форма. Мы дополняем проверку сравнением двух введенных паролей. Если они не похожи то возвращаем ошибку. Теперь эту форму импортируйте в файл views.py.

Добавим классы для страницы профиля и для логаута в файл views.py:

class LogoutView(View):
    def dispatch(self, request, *args, **kwargs):
        logout(request)
        return redirect("/")


class ProfileView(TemplateView):
    template_name = "registration/profile.html"

Если у вас что-то красное, может вы что-то не импортировали. Проверьте:

from django.contrib import messages
from django.contrib.auth import logout
from django.contrib.auth.models import User
from django.shortcuts import redirect
from django.shortcuts import render
from django.urls import reverse
from django.views import View
from django.views.generic import TemplateView

Добавим сразу все нужные урлы в urls.py:

    url(r'^accounts/login/$', login, name="login"),
    url(r'^accounts/logout/$', LogoutView.as_view(), name="logout"),
    url(r'^accounts/register/$', RegisterView.as_view(), name="register"),
    url(r'^accounts/profile/$', ProfileView.as_view(), name="profile"),

И импортируем View классы и функцию login:

from django.contrib.auth.views import login

Теперь нам нужно создать три html-шаблона для трех страниц. Создадим их в папке templates/registration. Создайте папку registration, если ее нет в templates. 

login.html:

{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
    <div class="container content" style="text-align: center;">
        <div style="display: inline-block; width: 300px;">
            <h2>Вход в мини Соцсеть</h2>
            <form method="post" class="bootstrap-form">
                {% csrf_token %}
                {{ form.as_p }}
                <input type="submit" class="btn btn-success" value="Войти">
            </form>
        </div>
    </div>
{% endblock %}

register.html:

{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
    <div class="container content" style="text-align: center;">
        <div style="width: 300px; display: inline-block;">
            <h2>Регистрация</h2>
            <form method="post" class="bootstrap-form">
                {% csrf_token %}
                {{ form.as_p }}
                <input type="submit" class="btn btn-success" value="Регистрация">
            </form>
        </div>
    </div>
{% endblock %}

profile.html:

{% extends 'base.html' %}
{% block content %}
    <div class="container content">
        <h2>Мой профиль</h2>
    </div>
{% endblock %}

Если вы заметели я добавил в форму class="bootstarp-form". Всем потомкам форм с таким классом мы через jQuery будем добавлять класс "form-control", который делает поле ввода, красивой. Вот этот код в файле scripts.js:

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

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

Также я добавил в css файл :

ul.errorlist{
    color: red;
    list-style: none;
    font-size: 12px;
}

Чтобы показывать разные сообщения, мы будем использовать пакет messages в комплекте django. Его использование есть в RegisterView, после регистрации мы показываем сообщение. Для отображения сообщения во всех страницах, мы добавим в base.html:

<body>
    {% include 'blocks/navbar.html' %}
    {% include 'blocks/messages.html' %}

    {% block content %}
    {% endblock %}
</body>

в папке blocks создадим еще один файл messages.html:

{% for message in messages %}
    {% if message.tags == 'success' %}
        <div class="alert alert-success" role="alert">
            {{ message }}
        </div>
    {% elif message.tags == 'error' %}
        <div class="alert alert-danger" role="alert">
            {{ message }}
        </div>
    {% endif %}
{% endfor %}

Теперь регистрация, логин и профиль готовы. Проверьте

3. Заполнение личных данных

Сделеам так: после регистрации, перенесем пользователя на страницу логина. Когда пользователь впервые войдет в соцсеть мы покажем ему форму заполнения личных данных. 

Откроем  RegisterView и после успешной регистрации вернем пользователяна страницу логина, а не на главную

return redirect(reverse("login"))

После логина, джанго перенаправляет пользователя на страницу профиля. Там пока у нас пусто. Давайте заполним страницу профиля так:

registartion/profile.html:

{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
    <div class="container content">
        <h2>{{ selected_user.get_full_name }}</h2>

        <div class="row" style="margin-left: 20px;">
            <div class="col5">
                {% if selected_user.profile.avatar.name %}
                    <img src="{{ selected_user.profile.avatar.url }}" class="avatar-img img-thumbnail">
                {% else %}
                    <img src="{% static 'img/user.jpg' %}" class="avatar-img img-thumbnail">
                {% endif %}
                <br>
                <br>
                {% if selected_user.id == user.id %}
                    <a href="{% url 'edit_profile' %}" class="btn btn-sm btn-info">Редактировать профиль</a>
                {% endif %}
            </div>
            <div class="col">
                <dl class="row">
                    <dt class="col-sm-3 text-right">Полное имя</dt>
                    <dd class="col-sm-9">{{ selected_user.get_full_name }}</dd>

                    <dt class="col-sm-3 text-right">Email</dt>
                    <dd class="col-sm-9">{{ selected_user.email }}</dd>

                    <dt class="col-sm-3 text-right">Город</dt>
                    <dd class="col-sm-9">{{ selected_user.profile.city }}</dd>

                    <dt class="col-sm-3 text-right">Дата рождения</dt>
                    <dd class="col-sm-9">{{ selected_user.profile.birth_date|date:"d M Y" }}</dd>

                    <dt class="col-sm-3 text-right">Пол</dt>
                    <dd class="col-sm-9">{{ selected_user.profile.get_gender_display }}</dd>

                    <dt class="col-sm-3 text-right">Статус отношений</dt>
                    <dd class="col-sm-9">{{ selected_user.profile.get_relationship_display }}</dd>

                    <dt class="col-sm-3 text-right">О себе</dt>
                    <dd class="col-sm-9"><em style="font-family: 'Times New Roman', serif;">{{ selected_user.profile.bio|linebreaks }}</em></dd>
                </dl>
            </div>
        </div>
    </div>
{% endblock %}

Здесь selected_user - это выбранный пользователь, профиль которого показывается. Этот шаблон в будущем мы будем использовать для просмотра чужих профилей тоже. А user - это текущий пользователь. selected_user.profile - так мы обращаемся к связанному объекту Profile. 

Как видно из кода, если у пользователя нет аватарки, мы показываем статическую картинку 'img/user.jpg'. Она вот такая:

Дальше мы показываем ссылку "Редактировать профиль", если selected_user равно user, т.е. если мы просматриваем свой профиль, то мы видим кнопку редактирования. 

А внизу страницы информация о пользователе. 

Теперь, нам нужно поменять ProfileView так, чтобы он передавал в контекст шаблона переменную selected_user. Для этого откройте класс ProfileView и добавьте метод dispatch:

class ProfileView(TemplateView):
    template_name = "registration/profile.html"

    def dispatch(self, request, *args, **kwargs):
        if not Profile.objects.filter(user=request.user).exists():
            return redirect(reverse("edit_profile"))
        context = {
            'selected_user': request.user
        }
        return render(request, self.template_name, context)

Вначале мы проверяем есть ли в БД объект Profile связанный с текущим пользователем, т.е. заполнял ли пользователь свой профиль уже. Если нет, то мы его редиректим(перенаправляем) на страницу редактирования профиля. Этой страницы у нас пока нет. Ниже мы его создадим. Дальше, в создаем переменную context, в котором указываем selected_user как request.user - это текущий пользователь. 

Чтобы редактировать профиль, нам нужно создать джанго-форму. Откроем файл forms.py и добавляем туда класс ProfileForm:

from mysite.models import Profile

class ProfileForm(forms.ModelForm):

    class Meta:
        model = Profile
        exclude = ['user']

Вначале мы импортировали нашу модель Profile из файла models. Класс ProfileForm наследует класс ModelForm. А ModelForm умеет создавать форму на основе указанной модели, в нашем случае Profile. Мы указали чтобы он не включал поле 'user'. В модели Profile есть поле user, который ссылается на объект Пользователя, и определяет какому пользователя принадлежит этот профиль. Это поле мы будем вручную выставлять равным текущему пользователю. 

Форму создали, теперь создадим view для страницы редактирования профиля. Откроем views.py и добавим туда класс EditProfileView:

class EditProfileView(TemplateView):
    template_name = "registration/edit_profile.html"

    def dispatch(self, request, *args, **kwargs):
        form = ProfileForm(instance=self.get_profile(request.user))
        if request.method == 'POST':
            form = ProfileForm(request.POST, request.FILES, instance=self.get_profile(request.user))
            if form.is_valid():
                form.instance.user = request.user
                form.save()
                messages.success(request, u"Профиль успешно обновлен!")
                return redirect(reverse("profile"))
        return render(request, self.template_name, {'form': form})

    def get_profile(self, user):
        try:
            return user.profile
        except:
            return None

Сюда нужно импортировать Profile из файла models.py и ProfileForm из файла forms.py. 

При обращении пользователя на эту страницу, мы создаем форму ProfileForm и просто отображаем страницу с этой формой. А если метод POST, т.е. пользователь заполнил и отправил форму, мы снова создаем форму ProfileForm уже с отправленными пользователем данными и файлами, проверяем правильно ли заполнена форма, и если правильно то сохраняем форму. Эта форма при сохранении сохраняет объект Profile в базу данных. К этому объекту мы можем обратиться через form.instance. И как видно из кода, мы сначала объекту Profile устанавливаем пользователя. 

А что такое (instance=self.get_profile(request.user)) при создании формы, спросите вы? А вот щас я отвечу. При создании формы, она не связана ни с каким объектом из БД. И при сохранении форма создаст новый объект. Но, а если мы хотим редактировать просто старый профиль, мы должны указать форме ссылку на эту профиль. После этого он во первых не будет создавать новый объект в БД, а просто менять старый, и во вторых он заполнит форму имеющимися старыми данными. 

Метод get_profile возвращает объект Profile если он есть для текущего пользователя, иначе возвращает None. 

Теперь, создадим html шаблон для этой страницы. И он будет в файле registartion/edit_profile.html:

{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
    <div class="container content" style="text-align: center;">
        <div style="display: inline-block; width: 400px;">
            <h2>Редактировать профиль</h2>
            <form method="post" class="bootstrap-form" enctype="multipart/form-data">
                {% csrf_token %}
                {{ form.as_p }}
                <input type="submit" class="btn btn-success" value="Сохранить">
            </form>
        </div>
    </div>
{% endblock %}

Заметьте, что в теге <form> мы добавили атрибут enctype="multipart/form-data". Это значит что через эту форму можно загружать не только данные, но и файлы. Это обязательно. 

Также нужно добавить url для этой страницы:

url(r'^accounts/profile/edit/$', EditProfileView.as_view(), name="edit_profile"),

Кстати, когда вы загружаете аватарку пользователя, то он сохраняется в папку медиа файлов, путь к которой указан в файле settings.py в переменной MEDIA_ROOT. Обычно она равна 

MEDIA_ROOT = os.path.join(BASE_DIR, "media")

Т.е. папка media внутри корня вашего проета. После загрузки аватарки, файл появится в этой папке. А чтобы через бразуер получить эти файлы, мы обращаемся через специальный медиа url:

MEDIA_URL = '/media/'

добавьте их, если у вас нет. и еще добавьте в urls.py ссылку для медиа-файлов, чтобы в самом конце было так:

] + static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS) +\
              static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Вот и все. И еще в файле styles.css  я добавил один стиль:

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

 

5373 0
Alisher Alikulov