🐍🚀 Создаем рекрутинговый портал на Django: часть 2

В этой части: добавление обложек проектов с фронтенда, создание системы авторизации, разработка основной функциональности приложения «Пользователи» и связь проектов с профилями.
🐍🚀 Создаем рекрутинговый портал на 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 }} &#215;</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

    

Запустите сервер – можно создавать профили в админ-панели. На главной странице теперь выводятся профили разработчиков:

Профили на главной странице ITfinder
Профили на главной странице ITfinder

Карточки в разделе «Проекты» пока не связаны с их авторами – мы займемся этим на следующем этапе:

Проекты
Проекты

Четвертый этап

Для связи проектов с профилями разработчиков добавим импорт модели профиля в projects/models.pyfrom 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.

***

Материалы по теме

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ