🐍🎸 Курс Django: Сложная форма с кастомной капчей
Делаем анкету с различными виджетами, защищаем форму с помощью капчи, автоматически отправляем данные и вложенные файлы на email.

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

Анкета кандидата – форма с выпадающим списком, чекбоксами, радиокнопками, полем для загрузки файла резюме и капчей. Основная функциональность:
- Кастомный рендеринг – все поля анкеты выводятся по отдельности, что представляет собой определенную сложность при обработке выпадающего списка, радиокнопок и чекбоксов.
- Кастомная капча и ее обновление без перезагрузки страницы.
- Автоматическая отправка полученных данных (включая файл резюме) на нужный 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-формы на имейл – в реальном приложении так можно реализовать подписку на вакансии, новости и т.п.
В следующей статье будем подробно разбирать работу с шаблонами. Весь код для этого проекта – в репозитории.