🐍🎸 Курс Django: Портфолио разработчика

Покажем, как сделать личный сайт с анимированным портфолио, сортировкой работ по категориям на фронтенде, контактной формой, резюме и отзывами работодателей.

Обзор проекта

Приложение состоит из 3 основных разделов – главной страницы, резюме и секции «Контакты». На главной странице выводятся все работы, перечень услуг (со списками используемых инструментов), и отзывы заказчиков:

Главная страница

Раздел «Обо мне», по сути, является резюме владельца портфолио – здесь можно разместить подробные сведения об образовании, опыте работе и уровне профессиональных навыков:

Резюме разработчика

В разделе «Контакты» перечислены всевозможные способы связи с владельцем портфолио, но самое главное – там еще есть форма для отправки сообщения:

Контактная форма

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

А теперь расскажем, как это все сделать.

🐍 Библиотека питониста
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»
🐍🎓 Библиотека собеса по Python
Подтянуть свои знания по Python вы можете на нашем телеграм-канале«Библиотека собеса по Python»
🧩🐍 Библиотека задач по Python»
Интересные задачи по Python для практики можно найти на нашем телеграм-канале«Библиотека задач по Python»

Ход работы

Весь код есть в репозитории: просто скопируйте, если что-то не получается.

Установка Django, Pillow и настройки проекта

Для хранения ссылок на изображения (и автоматической загрузки изображений в папку media) нужна библиотека Pillow – установите ее сразу после Django:

python -m venv myportfolio\venv
cd myportfolio
venv\scripts\activate
pip install django
pip install pillow
django-admin startproject config .
python manage.py startapp portfolio

Добавьте приложение portfolio в INSTALLED_APPS в config/settings.py:

INSTALLED_APPS = [
    'portfolio.apps.PortfolioConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Также добавьте в config/settings.py настройки для вывода сообщений:

MESSAGE_TAGS = {
        messages.DEBUG: 'alert-secondary',
        messages.INFO: 'alert-info',
        messages.SUCCESS: 'alert-success',
        messages.WARNING: 'alert-warning',
        messages.ERROR: 'alert-danger',
}

И пути к папкам static и media:

STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / "media"

Для удобства еще можно изменить язык админки на русский, а время – на московское:

LANGUAGE_CODE = 'ru'

TIME_ZONE = 'Europe/Moscow'

Чтобы Django мог работать с папками static и media, нужно добавить эти пути в файл config/urls.py:

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
]

urlpatterns += static(settings.MEDIA_URL, 
    document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, 
    document_root=settings.STATIC_ROOT)

Создание базы данных и аккаунта админа

Инициализируйте базу и создайте аккаунт суперпользователя:

python manage.py migrate
python manage.py createsuperuser

Теперь можно приступить к созданию нужных таблиц в БД. Сохраните эти модели в файле portfolio/models.py:

from django.db import models

class Skill(models.Model):
    name = models.CharField(max_length=30)
    level = models.CharField(max_length=3)

    class Meta:
        ordering = ['id']
        verbose_name = 'Навык'
        verbose_name_plural = 'Навыки'

    def __str__(self):
        return self.name


class Category(models.Model):
    engname = models.CharField(max_length=25)
    rusname = models.CharField(max_length=25)
 
    class Meta:
        ordering = ['id']
        verbose_name = 'Категория'
        verbose_name_plural = 'Категории'

    def __str__(self):
        return self.rusname

class Work(models.Model):
    title = models.CharField(max_length=150)
    slug = models.SlugField(max_length=150, unique=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='works')
    image = models.ImageField(upload_to='works')
    description = models.TextField()
    stack = models.TextField()
    link = models.URLField(max_length=200)
 
    class Meta:
        ordering = ['-id']
        verbose_name = 'Работа'
        verbose_name_plural = 'Работы'

    def __str__(self):
        return self.title


class Service(models.Model):
    name = models.CharField(max_length=25)
    icon = models.CharField(max_length=50)
    description = models.CharField(max_length=200)

    class Meta:
        ordering = ['id']
        verbose_name = 'Сервис'
        verbose_name_plural = 'Виды сервиса'

    def __str__(self):
        return self.name    

class Item(models.Model):
    name = models.CharField(max_length=150)
    service = models.ForeignKey(Service, on_delete=models.CASCADE)

    class Meta:
        ordering = ['-id']
        verbose_name = 'Инструмент'
        verbose_name_plural = 'Инструменты'

    def __str__(self):
        return self.name    



class Author(models.Model):
    name = models.CharField(max_length=15)
    lastname = models.CharField(max_length=15)
    about = models.TextField()
    skills = models.ManyToManyField(Skill, related_name='author')
    image = models.ImageField(upload_to='author')

    class Meta:
        ordering = ['-id']
        verbose_name = 'Автор'
        verbose_name_plural = 'Авторы'

    def __str__(self):
        return f'{self.name} {self.lastname}'


class Message(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    subject = models.CharField(max_length=100)
    message = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Сообщение'
        verbose_name_plural = 'Сообщения'

    def __str__(self):
        return f'Сообщение от {self.name}: {self.subject}'   

class Testimony(models.Model):
    name = models.CharField(max_length=15)
    lastname = models.CharField(max_length=15)  
    image = models.ImageField(upload_to='clients') 
    text = models.TextField()     

    class Meta:
        ordering = ['-id']
        verbose_name = 'Заказчик'
        verbose_name_plural = 'Заказчики'

    def __str__(self):
        return f'{self.name} {self.lastname}'

А затем подготовьте и примените миграции (изменения в структуре БД):

python manage.py makemigrations
python manage.py migrate

Чтобы с БД можно было работать в админке, модели нужно зарегистрировать в файле portfolio/admin.py:

from django.contrib import admin
from .models import Skill, Author, Category, Testimony, Work, Item, Service, Message


class ItemInline(admin.TabularInline):
    model = Item


class ServiceAdmin(admin.ModelAdmin):
    inlines = [ItemInline]



admin.site.register(Category)
admin.site.register(Testimony)
admin.site.register(Skill) 
admin.site.register(Service, ServiceAdmin)
admin.site.register(Author)
admin.site.register(Work)
admin.site.register(Item)
admin.site.register(Message)

Разберемся, что и как определяют эти модели.

Skill – навыки. Название и уровень навыка позволяют рендерить на фронтенде эти данные:

Визуализацией навыков занимается Bootstrap

Данные из Category используются не только для указания категории, к которой принадлежит проект, но и для фильтрации работ с помощью плагина Isotope.js:

<div id="filters" class="filters">
 <a href="#" data-filter="*" class="active">Все</a>
 {% for category in categories %}
 <a href="#" data-filter=".{{ category.engname }}">{{ category.rusname }}</a>
 {% endfor %}
</div>

Work, как и модели Author и Testimony, использует ImageField для автоматической загрузки изображений в соответствующие поддиректории media, а также для создания и хранения ссылок на эти изображения. Работу ImageField обеспечивает библиотека Pillow:

image = models.ImageField(upload_to='works')

Для хранения ссылок на готовые сайты используется поле URLField.

В модели Service хранятся виды услуг. Поскольку в оформлении портфолио используются иконки BoxIcons, в поле icon нужно сохранять название нужной иконки в этом наборе, например, bx bx-laptop. При использовании другого набора, скажем, Font Awesome, нужно вводить названия иконок в соответствующем этому набору формате.

Item – вид инструмента, относящегося к конкретному виду услуг. С помощью данных из Item стек инструментов можно рендерить в виде списка с иконками:

{% for item in service.item_set.all %}
<li><span class='bx bx-chevron-right'></span>{{ item.name }}</li>
{% endfor %}

Другой способ отрендерить список в нужном стиле – сохранить список с HTML-тегами и Bootstrap стилями в базе, и вывести его в шаблоне с помощью тега |safe:

<h4 class="h4 mb-3">Технологический стек</h4>
{{ work.stack|safe }} 
Так нужно сохранить список в базе

Message сохраняет и показывает (в админке) все полученные сообщения. Чтобы получать сообщения на почту, нужно подключить собственный (сложнее) или сторонний (гораздо проще) SMTP-сервер. Здесь показано, как получать сообщения с помощью SMTP Яндекса.

Отзывы заказчиков сохраняются в таблице Testimony, а данные о владельце портфолио – в Author.

Представления и маршруты

Все представления в этом проекте – функциональные. Написание представлений на основе классов мы рассмотрим в одной из последующих статей. Эти представления нужно сохранить в файле portfolio/views.py:

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Author, Category, Work, Service, Testimony, Item, Message

def index(request):
    categories = Category.objects.all()
    works = Work.objects.all()
    services = Service.objects.all()
    testimonies = Testimony.objects.all()

    context = {
       'categories': categories,
       'works': works,
       'services': services,
       'testimonies': testimonies,
   }

    return render(request, 'index.html', context)

def about(request):
    author = Author.objects.get()
    return render(request, 'about.html', {'author': author})


def work_detail(request, slug):
    work = get_object_or_404(Work, slug=slug)
    testimonies = Testimony.objects.all()
    context = {
        'work': work,
        'testimonies': testimonies,
    }
    return render(request, 'work_detail.html', context)


def contact(request):
    if request.method == 'POST':
        msg = Message(
            name=request.POST['name'],  
            email=request.POST['email'],
            subject=request.POST['subject'],  
            message=request.POST['message']
         )
        msg.save()
        messages.success(request, 'Сообщение отправлено!')
        return redirect('contact') 
  
    return render(request, 'contact.html')

Представление index передает в шаблон index.html все записи, сохраненные в таблицах Work, Category, Service и Testimony, потому что на главной странице выводятся все данные, без сокращений. Если портфолио объемное, имеет смысл ограничить количество работ с помощью среза works = Work.objects.all()[:3], а все работы вывести на отдельной странице с пагинацией. В следующем проекте, посвященном разработке блога, мы разберем процесс создания пагинации.

В таблице Author хранится всего одна запись, поэтому для ее извлечения используется запрос author = Author.objects.get(). В случае создания некой платформы для размещения портфолио, где авторов множество, в эту функцию нужно передавать id или username конкретного владельца.

Представление work_detail выводит информацию о каждом проекте в отдельности, причем для извлечения данных используется не id работы, а слаг:

work = get_object_or_404(Work, slug=slug)

Благодаря использованию слага, ссылка выглядит как http://site.com/work/design-studio/, а при использовании id она бы выглядела как http://site.com/work/5/.

Представление contact обеспечивает обработку данных из контактной формы. В проекте используется обычная HTML-форма, которая самостоятельно отслеживает заполнение полей при помощи тега required. В Django есть отличный модуль для работы с формами, который предоставляет всю возможную функциональность для валидации данных (однако оформление формы разработчику все равно придется делать самостоятельно). В одном из последующих проектов мы разберем использование Django-форм и все способы придания им привлекательного внешнего вида.

Чтобы Django мог обработать конкретную форму (в случае, если их на странице несколько), нужно добавить ссылку на нужное представление в form action:

<form action="{% url 'contact' %}"

Кроме того, Django обязательно нужен токен внутри формы:

{% csrf_token %}

Все представления приводятся в действие маршрутами. Сохраните эти маршруты в portfolio/urls.py:

from django.urls import path
from .views import index, contact, about, work_detail

urlpatterns = [
    path('', index, name='index'),
    path('about/', about, name='about'),
    path('work/<slug:slug>/', work_detail, name='work_detail'),
    path('contact/', contact, name='contact'),

И не забудьте включить маршруты portfolio в config/urls.py:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('portfolio.urls')),

Шаблоны

Для вывода данных на фронтенде портфолио использует несколько шаблонов:

  • base.html – основной шаблон проекта. Здесь определены навигация и футер (поскольку они выглядят одинаково на всех страницах сайта), а также подключены все нужные скрипты на JavaScript, HTML/CSS-стили Bootstrap, шрифт Google, favicon и т. д.
  • index.html – как и все последующие шаблоны, наследует все стили base.html с помощью тега {% extends 'base.html' %}. Выводит данные обо всех проектах, услугах, категориях и отзывах.
  • work_detail.html – показывает подробности реализации каждого проекта.
  • about.html – резюме владельца.
  • contact.html – все контакты разработчика и форма для отправки сообщений.

Подведем итоги

При желании готовое портфолио можно экспортировать в статические HTML/CSS/JS-файлы с помощью django-distill, а для обработки контактной формы подключить сервис вроде Formspree. В таком виде портфолио можно будет разместить на GitHub Pages. В одном из последующих проектов мы рассмотрим процесс преобразования динамического Django-сайта в статический.

***

Содержание курса

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

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
28 июня 2018

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...