🐍🚀 Создаем рекрутинговый портал на Django: часть 2
В этой части: добавление обложек проектов с фронтенда, создание системы авторизации, разработка основной функциональности приложения «Пользователи» и связь проектов с профилями.

Третий этап
Основная часть работы над приложением «Проекты» была выполнена на предыдущем этапе – осталось реализовать добавление изображений. Для этого нужно внести дополнения в модель Projects:
image = models.ImageField(null=True, blank=True, default="project_images/default.jpg", upload_to='project_images')
А затем выполнить миграции:
python manage.py makemigrations python manage.py migrate
По умолчанию Джанго будет сохранять изображения прямо в корневую директорию проекта, а искать – в папке static. Гораздо удобнее завести для изображений отдельную папку media в корне проекта (а не внутри static). Создайте директорию media, внутри нее папку project_images и поместите в нее файл default.jpg.
Чтобы дать Джанго знать, куда сохранять изображения, сделаем нужные настройки в settings.py:
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/'
В файл urls.py добавим импорт:
from django.conf.urls.static import static
И маршруты к нужным папкам:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
Осталось добавить ссылки на изображения в шаблон projects.html:
<img class="project__thumbnail" src="{{ project.image.url }}" alt="скриншот проекта" />
И в single-project.html:
<img class="singleProject__preview" src="{{ project.image.url }}" alt="скриншот проекта" />
Пока что добавлять изображения в карточки проектов можно только в панели администрирования. Чтобы дать пользователям возможность добавлять изображения на стороне фронтенда, сделаем новый шаблон project_form.html:
{% extends 'base.html' %} {% block content %} <main class="formPage my-xl"> <div class="content-box"> <div class="formWrapper"> <a class="backButton" href="#"><i class="im im-angle-left"></i></a> <br> <form class="form" method="POST" enctype="multipart/form-data"> {% csrf_token %} {% for field in form %} <div class="form__field"> <label for="formInput#text">{{ field.label }}</label> {{ field }} </div> {% endfor %} <div class="form__field"> {% for tag in project.tags.all %} <div class="project-tag tag tag--pill tag--main" data-tag="{{ tag.id }}" data-project="{{ project.id }}">{{ tag.name }} ×</div> {% endfor %} </div> <div class="form__field"> <label for="formInput#text">Теги</label> <textarea class="input" name="newtags" placeholder="Добавьте собственные теги, если их нет в списке"></textarea> </div> <input class="btn btn--sub btn--lg my-md" type="submit" value="Сохранить" /> </form> </div> </div> </main> {% endblock %}
Кроме того, внесем дополнения в forms.py:
from django.forms import ModelForm from django import forms from .models import Project class ProjectForm(ModelForm): class Meta: model = Project fields = ['title', 'slug', 'image', 'tags', 'description', 'demo_link', 'source_link'] labels = { 'title': 'Название проекта', 'slug': 'Слаг', 'image':'Скриншот проекта', 'tags':'Теги', 'description':'Описание проекта', 'demo_link':'Демо-версия', 'source_link':'Исходный код на GitHub' } widgets = { 'tags': forms.CheckboxSelectMultiple(), } def __init__(self, *args, **kwargs): super(ProjectForm, self).__init__(*args, **kwargs) for name, field in self.fields.items(): field.widget.attrs.update({'class': 'input'})
Для сохранения изображений с фронтенда добавим FILES
в функции createProject
и updateProject
:
def createProject(request): form = ProjectForm() if request.method == 'POST': form = ProjectForm(request.POST, request.FILES) if form.is_valid(): form.save() return redirect('projects') context = {'form': form} return render(request, 'projects/project_form.html', context) def updateProject(request, pk): project = Project.objects.get(id=pk) form = ProjectForm(instance=project) if request.method == 'POST': form = ProjectForm(request.POST, request.FILES, instance=project) if form.is_valid(): form.save() return redirect('projects') context = {'form': form} return render(request, 'projects/project_form.html', context)
Создать проект можно по прежней ссылке – http://localhost:8000/create-project/, однако в навигационную панель мы эту ссылку добавлять не станем – позже она будет доступна в профиле пользователя.
Приложение Users – «Пользователи»
Начнем разработку секции «Пользователи» с создания нового приложения:
python manage.py startapp users
Как и в случае с projects, приложение нужно зарегистрировать в INSTALLED_APPS
(settings.py):
'users.apps.UsersConfig'
Для вывода профилей нам понадобятся два шаблона – profiles.html и user-profile.html, поместите их в папку users/templates/users.
Теперь создадим нужные функции представления – для вывода всех профилей, просмотра отдельного профиля и фильтрации профилей по скиллам. Сохраните этот код в файле users/views.py:
from django.shortcuts import render, get_object_or_404 from .models import Profile, Skill def profiles(request): profiles = Profile.objects.all() context = {'profiles': profiles} return render(request, 'users/profiles.html', context) def userProfile(request, pk): profile = Profile.objects.get(id=pk) main_skills = profile.skills.all()[:2] extra_skills = profile.skills.all()[2:] context = {'profile': profile, 'main_skills': main_skills, "extra_skills": extra_skills} return render(request, 'users/user-profile.html', context) def profiles_by_skill(request, skill_slug): skill = get_object_or_404(Skill, slug=skill_slug) profiles = Profile.objects.filter(skills__in=[skill]) context = { "profiles": profiles } return render(request, "users/profiles.html", context)
Создайте файл users/urls.py и сохраните в нем эти маршруты:
from django.urls import path from . import views urlpatterns = [ path('', views.profiles, name="profiles"), path('profile/<str:pk>/', views.userProfile, name="user-profile"), path('skill/<slug:skill_slug>', views.profiles_by_skill, name="skill"), ]
Внесите изменения в urlpatterns
в главном urls.py в корневой директории проекта itfinder:
path('projects/', include('projects.urls')), path('', include('users.urls')),
Измените ссылку на главную страницу в шаблоне navbar.html – вставьте {% url 'profiles' %}
.
И, наконец, сохраните нужные модели в users/models.py:
from django.db import models from django.contrib.auth.models import User import uuid class Skill(models.Model): name = models.CharField(max_length=50, blank=True, null=True) slug = models.SlugField() description = models.TextField(null=True, blank=True) created = models.DateTimeField(auto_now_add=True) id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False) def __str__(self): return str(self.name) class Profile(models.Model): user = models.OneToOneField( User, on_delete=models.CASCADE, null=True, blank=True) name = models.CharField(max_length=50, blank=True, null=True) email = models.EmailField(max_length=50, blank=True, null=True) username = models.CharField(max_length=50, blank=True, null=True) city = models.CharField(max_length=50, blank=True, null=True) intro = models.CharField(max_length=200, blank=True, null=True) bio = models.TextField(blank=True, null=True) image = models.ImageField( null=True, blank=True, upload_to='profile_images', default="profile_images/default.jpg") skills = models.ManyToManyField(Skill, blank=True) github = models.CharField(max_length=100, blank=True, null=True) twitter = models.CharField(max_length=100, blank=True, null=True) linkedin = models.CharField(max_length=100, blank=True, null=True) youtube = models.CharField(max_length=100, blank=True, null=True) website = models.CharField(max_length=100, blank=True, null=True) created = models.DateTimeField(auto_now_add=True) id = models.UUIDField(default=uuid.uuid4, unique=True, primary_key=True, editable=False) def __str__(self): return str(self.username) class Meta: ordering = ['created']
Теперь создайте и примените миграции:
python manage.py makemigrations python manage.py migrate
Запустите сервер – можно создавать профили в админ-панели. На главной странице теперь выводятся профили разработчиков:
Карточки в разделе «Проекты» пока не связаны с их авторами – мы займемся этим на следующем этапе:
Четвертый этап
Для связи проектов с профилями разработчиков добавим импорт модели профиля в projects/models.py – from users.models import Profile
и внесем дополнения в модель проекта:
owner = models.ForeignKey(Profile, null=True, blank=True, on_delete=models.CASCADE)
Подготовьте и выполните миграции:
python manage.py makemigrations python manage.py migrate
В панели управления выберите разработчиков-авторов для проектов, а в шаблонах projects/projects.html и projects/single-project.html вставьте ссылку на автора:
{% url 'user-profile' project.owner.id %}
Запустите сервер и перейдите на страницу «Проекты» – у каждого проекта теперь есть автор, и в профилях разработчиков отображаются их проекты:
Сигналы
Сигналы обеспечивают "прослушивание" действий пользователей и запуск нужных событий: к примеру, сигналы могут триггерить вспомогательные функции, которые нам потребуются в процессе создания, удаления и обновления учетных записей.
Создайте файл users/signals.py и сохраните в нем этот код, а в файл users/apps.py внесите это дополнение:
def ready(self): import users.signals
Теперь можно перейти к созданию всей нужной функциональности для учетных записей.
Регистрация, аутентификация, права доступа
Аутентификация подтверждает личность пользователя, а система авторизации предоставляет (или ограничивает) доступ к определенным функциям и разделам сайта, в зависимости от того зарегистрирован ли пользователь или нет.
Разработку системы аутентификации мы начнем со страницы, которая будет использоваться и для регистрации новых пользователей, и для входа на сайт. В папке users создайте новые шаблоны:
Добавьте в users/urls.py маршруты:
path('login/', views.loginUser, name="login"), path('logout/', views.logoutUser, name="logout"), path('register/', views.registerUser, name="register"), path('account/', views.userAccount, name="account"), path('edit-account/', views.editAccount, name="edit-account"), path('create-skill/', views.createSkill, name="create-skill"), path('update-skill/<slug:skill_slug>/', views.updateSkill, name="update-skill"), path('delete-skill/<slug:skill_slug>/', views.deleteSkill, name="delete-skill"),
В шаблон navbar.html добавьте ссылки на аккаунт, вход/регистрацию, выход:
{% url 'account' %} {% url 'login' %} {% url 'logout' %}
Доступ для авторизованных пользователей
Декоратор @login_required(login_url='login')
обеспечивает доступ к определенным функциям только для зарегистрированных и авторизованных пользователей. Сохраните эти представления в файле users/views.py:
@login_required(login_url='login') def userAccount(request): profile = request.user.profile skills = profile.skills.all() projects = profile.project_set.all() context = {'profile': profile, 'skills': skills, 'projects': projects} return render(request, 'users/account.html', context) @login_required(login_url='login') def editAccount(request): profile = request.user.profile form = ProfileForm(instance=profile) if request.method == 'POST': form = ProfileForm(request.POST, request.FILES, instance=profile) if form.is_valid(): form.save() return redirect('account') context = {'form': form} return render(request, 'users/profile_form.html', context) @login_required(login_url='login') def createSkill(request): profile = request.user.profile form = SkillForm() if request.method == 'POST': form = SkillForm(request.POST) if form.is_valid(): skill = form.save(commit=False) skill_slug = request.POST.get('slug') skill_description = request.POST.get('description') profile.skills.get_or_create(name=skill, slug=skill_slug, description=skill_description) messages.success(request, 'Навык добавлен') return redirect('account') context = {'form': form} return render(request, 'users/skill_form.html', context) @login_required(login_url='login') def updateSkill(request, skill_slug): profile = request.user.profile skill = profile.skills.get(slug=skill_slug) form = SkillForm(instance=skill) if request.method == 'POST': form = SkillForm(request.POST, instance=skill) if form.is_valid(): form.save() messages.success(request, 'Навык успешно обновлен') return redirect('account') context = {'form': form} return render(request, 'users/skill_form.html', context) @login_required(login_url='login') def deleteSkill(request, skill_slug): profile = request.user.profile skill = profile.skills.get(slug=skill_slug) if request.method == 'POST': skill.delete() messages.success(request, 'Навык успешно удален') return redirect('account') context = {'object': skill} return render(request, 'delete_template.html', context)
Помимо функций userAccount, editAccount, createSkill, updateSkill, deleteSkill мы добавим декоратор к функциям в файле views.py в приложении «Проекты» – поскольку добавлять, редактировать и удалять проекты могут только авторизованные пользователи. Функции createSkill, updateSkill, deleteSkill обеспечивают добавление, редактирование и удаление пользователями скиллов в своих профилях:
Удаление скиллов и проектов обрабатывает шаблон delete_template.html – его нужно поместить в папку с глобальными шаблонами templates. Кроме того, создайте новый файл users/forms.py, содержащий нужные формы.
Чтобы включить юзернеймы разработчиков в URL, в шаблонах templates/users мы заменим project.owner.id
на project.owner
, a маршрут в urls.py на этот:
path('profile/<str:username>/', views.userProfile, name="user-profile"),
После чего добавим ссылку на аккаунт разработчика в форму создания нового проекта:
{% url 'account' %}
Автоматическая генерация слагов
Если пользователь решит добавить к своему проекту теги, которых еще нет в базе, для них нужно будет сгенерировать слаги. Для этого добавьте импорт в projects/models.py:
from django.utils.text import slugify
И эту функцию в класс Tag
:
def save(self, *args, **kwargs): value = self.name self.slug = slugify(value, allow_unicode=True) super().save(*args, **kwargs)
Весь код и тестовый контент для этого этапа – в репозитории ITfinder.