Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Обзор проекта
Приложение состоит из 3 основных разделов – главной страницы, резюме и секции «Контакты». На главной странице выводятся все работы, перечень услуг (со списками используемых инструментов), и отзывы заказчиков:

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

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

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

А теперь расскажем, как это все сделать.
Ход работы
Весь код есть в репозитории: просто скопируйте, если что-то не получается.
Установка 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 – навыки. Название и уровень навыка позволяют рендерить на фронтенде эти данные:

Данные из 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-сайта в статический.
Комментарии