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

Анкета кандидата – форма с выпадающим списком, чекбоксами, радиокнопками, полем для загрузки файла резюме и капчей. Основная функциональность:
- Кастомный рендеринг – все поля анкеты выводятся по отдельности, что представляет собой определенную сложность при обработке выпадающего списка, радиокнопок и чекбоксов.
- Кастомная капча и ее обновление без перезагрузки страницы.
- Автоматическая отправка полученных данных (включая файл резюме) на нужный email.
Модели
Три поля в форме – «Вакансия», «Сертификаты» и «Коммерческий опыт» – имеют список возможных ответов.
1. Сертификатов может быть несколько, или ни одного – для хранения множественных ответов в базе и их вывода в виде чекбоксов принято использовать поле и связь ManyToManyField:
class Certificate(models.Model):
name = models.CharField(max_length=100)
...
class Application(models.Model):
...
certificates = models.ManyToManyField(Certificate, blank=True)
2. Вакансии выводятся в виде раскрывающегося списка. В БД такие записи хранят в виде choices, которые, в свою очередь, определяются с помощью кортежей:
class Certificate(models.Model):
...
POSITIONS = [
('frontend', 'Frontend разработчик'),
('backend', 'Backend разработчик'),
('mobile', 'Mobile разработчик'),
('devops', 'DevOps инженер'),
('qa', 'Тестировщик'),
]
position = models.CharField(max_length=100, choices=POSITIONS, default='frontend')
...
3. Коммерческий опыт предполагает только два варианта ответа – либо он есть, либо его нет. Поэтому для хранения этих choices используется BooleanField:
class Application(models.Model):
...
COMMERCIAL_EXPERIENCE_CHOICES = [
(True, 'Есть'),
(False, 'Нет')
]
commercial_experience = models.BooleanField(choices=COMMERCIAL_EXPERIENCE_CHOICES, default=True)
...
4. Для загрузки файла резюме используется поле FileField. Django автоматически загружает файлы пользователей в поддиректорию media:
resume = models.FileField(upload_to='resumes/')
5. Информация, которую возвращает о себе модель Application, будет указана в email'e:
def __str__(self):
POSITIONS_DICT = dict(self.POSITIONS)
return f"{self.name} с образованием {self.education} прислал резюме на вакансию {POSITIONS_DICT.get(self.position)}"
Форма
В форме мы используем все поля модели Application, плюс добавляем поле для капчи (подробнее о капче ниже). Здесь же определяем минимальное и максимальное значение для поля «Возраст», а также используем встроенный валидатор FileExtensionValidator для определения допустимых форматов файла резюме:
class ApplicationForm(forms.ModelForm):
captcha = CaptchaField()
age = forms.IntegerField(min_value=18, max_value=35)
resume = forms.FileField(
validators=[FileExtensionValidator(allowed_extensions=['doc', 'docx', 'pdf'])],
error_messages={'invalid_extension': 'Файл должен иметь формат DOC, DOCX или PDF'}
)
В классе Meta определяем тип виджетов и устанавливаем значение по умолчанию empty_label для выпадающего списка. Если такое значение не установить, в верхнем поле списка будет выведен пунктир --------.
widgets = {
'position' : forms.Select(attrs={'empty_label': 'Frontend разработчик'}),
'commercial_experience': forms.RadioSelect(),
'certificates': forms.CheckboxSelectMultiple()
}
Для передачи текста плейсхолдеров, стиля form control и установления высоты текстовых полей переопределяем значения нужных атрибутов:
def __init__(self, *args, **kwargs):
super(ApplicationForm, self).__init__(*args, **kwargs)
self.fields['name'].widget.attrs['placeholder'] = 'Введите ваше имя'
self.fields['age'].widget.attrs['placeholder'] = 'Мы рассматриваем кандидатов от 18 до 35'
self.fields['education'].widget.attrs['placeholder'] = 'Колледж, вуз, курсы'
self.fields['education'].widget.attrs['rows'] = 3
self.fields['email'].widget.attrs['placeholder'] = 'Введите корректный email для связи'
self.fields['work_experience'].widget.attrs['placeholder'] = 'За последние 5 лет'
self.fields['work_experience'].widget.attrs['rows'] = 3
self.fields['projects'].widget.attrs['placeholder'] = 'Ссылки на ваши проекты'
self.fields['projects'].widget.attrs['rows'] = 3
self.fields['resume'].widget.attrs['accept'] = '.pdf,.doc,.docx'
for name, field in self.fields.items():
field.widget.attrs.update({'class': 'form-control'})
Метод get_message извлекает данные из только что заполненной формы, а send отсылает информацию (включая файл резюме) на нужный email (подробнее о настройках почтового сервера ниже):
def get_message(self):
subject = f'Добавлена анкета {self.instance.name}'
msg = str(self.instance)
return subject, msg
def send(self):
subject, msg = self.get_message()
email = EmailMessage(
subject=subject,
body=msg,
from_email=settings.EMAIL_HOST_USER,
to=[settings.RECIPIENT_ADDRESS],
)
email.attach_file(self.instance.resume.path)
email.send()
Представления
За валидацию формы, сохранение данных в БД и вызов метода отправки информации на email отвечает представление ApplicationView:
class ApplicationView(FormView):
form_class = ApplicationForm
template_name = 'application.html'
success_url = reverse_lazy('success')
def form_valid(self, form):
application = form.save()
form.send()
return super().form_valid(form)
def form_invalid(self, form):
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f'В поле {field} возникла ошибка: {error}')
return super().form_invalid(form)
Функция refresh_captcha (совместно со скриптом captcha.js) отвечает за обновление капчи:
def refresh_captcha(request):
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
raise Http404
new_key = CaptchaStore.pick()
to_json_response = {
"key": new_key,
"image_url": captcha_image_url(new_key),
}
return JsonResponse(to_json_response)
Представление SuccessView выводит сообщение об успешном сохранении данных:

Как сделать капчу в Django-приложении
Самые популярные сервисы обработки капчи – Google reCaptcha и hCaptcha. В 2022 году появился сервис Yandex SmartCaptcha. У капчи Яндекса только одно очевидное преимущество – она гарантированно будет работать на российских сайтах. Но, по сравнению с reCaptcha и hCaptcha, настройки SmartCaptcha гораздо сложнее, а пользователей обязательно нужно предупреждать об ее использовании на сайте.
Лучшая альтернатива капче, которая сохраняет и куда-то передает пользовательские данные – автономная капча. Такая капча устанавливается прямо в Django-приложение и не занимается сбором данных – просто предлагает пользователю ввести код с картинки. Самая удобная и гибкая капча для Django – django-simple-captcha.
Установка и кастомизация django-simple-captcha
Модуль устанавливается с помощью команды:
pip install django-simple-captcha
Внешний вид и тип задач капчи можно менять как угодно. Все настройки капчи делаются в settings.py. По умолчанию капча использует параметр CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' и показывает случайные символы:

При желании можно загружать случайные слова из словаря: CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.word_challenge'
Или предлагать простые задачки с помощью CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge' :

В django-simple-captcha настраивается абсолютно все: количество и цвет символов, интенсивность шума и степень искажения. Вместо текстовой капчи можно выбрать аудио. В нашем проекте используются случайные русскоязычные слова из заранее определенного списка. Необходимые настройки в settings.py выглядят так:
from config.utils import random_word # импорт кастомного генератора из config/utils.py
CAPTCHA_FONT_PATH = 'fonts/arial.ttf' # кириллический шрифт
CAPTCHA_CHALLENGE_FUNCT = random_word # определение кастомного генератора
Обновление капчи без перезагрузки страницы
Капча получается забористой – хотя в ней используются только русскоязычные слова, иногда их реально сложно прочитать. Настройки читаемости улучшать не следует – тогда капча не будет представлять никакой сложности для ботов. Но для реальных пользователей можно и нужно предусмотреть возможность обновления капчи без перезагрузки формы. За эту функциональность, помимо уже упомянутого представления refresh_captcha, отвечает скрипт captcha.js, подключаемый в шаблоне base.html, маршрут path('captcha/refresh/', refresh_captcha, name='refresh_captcha')
и кнопка с id="refresh-captcha"
.
Как отправить email из Django-приложения
Для отправки имейлов из Django надо либо установить и настроить собственный почтовый сервер (это довольно сложно), либо воспользоваться любым сторонним сервисом, который предоставляет доступ к SMTP для приложений. Такой доступ предоставляет, например, Яндекс.
Как получить пароль для приложения
Учетные данные обычного пользовательского аккаунта нельзя использовать для отправки имейлов из приложений. Чтобы подключить свое приложение к почтовому серверу, надо получить специальный пароль.
Нажмите на шестеренку и выберите Все настройки:

В настройках нажмите на Почтовые программы, отметьте IMAP:

Перейдите в Пароли приложений, затем в Безопасность:

И в разделе Пароли приложений создайте пароль для почты:

Теперь можно сделать все нужные настройки в settings.py:
- RECIPIENT_ADDRESS = 'admin@gmail.com' – это адрес, на который будут приходить сообщения. Адресов можно указать несколько, в виде списка.
- EMAIL_HOST = 'smtp.yandex.ru'
- EMAIL_PORT = 465
- EMAIL_USE_SSL = True
- DEFAULT_FROM_EMAIL = 'sender@yandex.ru' – адрес, который по умолчанию будет указан в поле "от кого".
- EMAIL_HOST_USER = 'yandex_user@yandex.ru' – адрес, в учетной записи которого вы создали пароль для приложения.
- EMAIL_HOST_PASSWORD = 'yourownpasswordhere' – тот самый пароль приложения.
- EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
Эти настройки используются модулем EmailMessage и методом send в форме ApplicationForm. Письма будут приходить с вложенными резюме:

Шаблоны и рендеринг формы
Приложение использует 3 шаблона:
Кастомный рендеринг формы происходит application.html. Для вывода чекбоксов используется цикл и счетчик forloop.counter, который подсчитывает итерации:
{% for checkbox in form.certificates %}
<div class="form-check">
<label for="id_tags_{{ forloop.counter }}">
<input type="checkbox"
name="certificates"
value="{{ forloop.counter }}"
class="form-check-input"
id="id_certificates_choice{{ forloop.counter }}"
value="{{ choice.id }}"/>
{{ checkbox.choice_label }}
</label>
</div>
{% endfor %}
При выводе выпадающего списка используются оба значения из кортежа:
<select name="position" class="form-select">
{% for value in form.position.field.choices %}
<option value="{{ value.0 }}">{{ value.1 }}</option>
{% endfor %}
</select>
При визуализации радиокнопок первое значение из кортежа используется в качестве value, второе в качестве label:
{% for value, label in form.commercial_experience.field.choices %}
<div>
<input type="radio" name="commercial_experience" value="{{ value }}">
<label>{{ label }}</label>
</div>
{% endfor %}
Подведем итоги
В ходе разработки этого проекта мы научились:
- Кастомизировать и обновлять капчу без перезагрузки страницы.
- Рендерить сложные виджеты с помощью пользовательских HTML/CSS стилей.
- Отправлять данные из Django-формы на имейл – в реальном приложении так можно реализовать подписку на вакансии, новости и т.п.
В следующей статье будем подробно разбирать работу с шаблонами. Весь код для этого проекта – в репозитории.
Комментарии