В качестве хобби, я решил открыть для себя мир веб-разработки, попутно освещая свои успехи, а может и неудачи на пути к просветлению…

В настоящее время я публично описываю процесс создания веб-сайта с нуля на Django и Bootstrap, где задача разработать такие приложения как: аккаунты, вопросы, статьи и книги.

В проекте решаются только реальные задачи, которые почему-то так любят обходить стороной в учебной литературе.

Создание веб-сайта с нуля на Django и Bootstrap. Аккаунты. Регистрация с подтверждением адреса электронной почты. Формы

Сергей Серов

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

А для того чтобы пользователи смогли различать друг друга, мы на финальной стадии регистрации добавим им цифровой код. Например, Анна-Мария #777.

Формы

Начнем с создания форм, в директории приложения accounts добавляем файл forms.py:

import re

from django import forms
from django.db.utils import IntegrityError
from django.contrib.auth.password_validation import validate_password

from core.utils import get_random

from .models import User
from .utils import Vcode


class JoinForm(forms.ModelForm):
    username = forms.CharField(label='Псевдоним или настоящее имя', min_length=2, max_length=50,
                               widget=forms.TextInput(attrs={'autofocus': ''}))
    email = forms.EmailField(label='Адрес электронной почты', max_length=254,
                             error_messages={'unique': 'Данный адрес электронной почты уже используется'})
    password = forms.CharField(label='Пароль', max_length=255, strip=False, widget=forms.PasswordInput)

    field_order = ['username', 'email', 'password']

    class Meta:
        model = User
        fields = ['email']

    def clean_username(self):
        username = self.cleaned_data.get('username')
        rus_alphabet = 'а-яА-ЯёЁ'
        eng_alphabet = 'a-zA-Z'
        alphabets = rus_alphabet + eng_alphabet
        if not re.search(r'^[{alphabets}][{alphabets}\d -]*[{alphabets}\d]$'.format(alphabets=alphabets), username):
            raise forms.ValidationError('Псевдоним может содержать только буквы латинского и кириллического алфавитов, '
                                        'цифры, пробелы и знаки дефиса. Начинаться с буквы и заканчиваться на цифру '
                                        'или букву')
        if re.search(r'[{rus_alphabet}]'.format(rus_alphabet=rus_alphabet), username) and \
                re.search(r'[{eng_alphabet}]'.format(eng_alphabet=eng_alphabet), username):
            raise forms.ValidationError('Псевдоним не может содержать одновременно буквы латинского и кириллического '
                                        'алфавитов (нельзя смешивать алфавиты)')
        if re.search(r' {2,}|-{2,}', username):
            raise forms.ValidationError('Псевдоним не может содержать два или более пробелов (и дефисов) подряд')
        if username.count(' ') > 3 or username.count('-') > 3:
            raise forms.ValidationError('Псевдоним не может содержать более трех пробелов (и дефисов) для разделения '
                                        'слов')
        return username

    def clean_password(self):
        # проверяем пароль с помощью списка валидаторов объявленного в settings.AUTH_PASSWORD_VALIDATORS
        password = self.cleaned_data.get('password')
        user = User(username=self.cleaned_data.get('username'), email=self.cleaned_data.get('email'))
        validate_password(password, user)
        return password

    def save(self, commit=True, request=None):
        user = super(JoinForm, self).save(commit=False)
        user.set_password(self.cleaned_data['password'])
        user.is_active = False
        if commit:
            # создаем список кодов от 100 до 999 и затем перемешиваем
            codes = [c for c in range(100, 999 + 1)]
            get_random().shuffle(codes)
            for code in codes:
                user.username = '{} #{}'.format(self.cleaned_data['username'], code)
                if not User.objects.filter(username=user.username).exists():
                    try:  # пробуем сохранить уникальный псевдоним
                        user.save()
                    except IntegrityError:
                        # начинаем заново, видимо нас опередили, БД сообщает, что значения не уникальны!
                        continue
                    break
            else:
                raise ValueError('Цикл кодов завершен, сгенерировать уникальный псевдоним не удалось')
            # отправляем проверочный код пользователю
            Vcode(request).send(user, 'join')
        return user


class JoinResendForm(forms.Form):
    email = forms.EmailField(label='Адрес электронной почты', max_length=254,
                             widget=forms.EmailInput(attrs={'autofocus': ''}))

    def __init__(self, *args, **kwargs):
        super(JoinResendForm, self).__init__(*args, **kwargs)
        self.user = None

    def clean_email(self):
        email = self.cleaned_data.get('email')
        try:
            self.user = User.inactive.get(email=email)
        except User.DoesNotExist:
            raise forms.ValidationError('Пользователя с данным адресом электронной почты не существует '
                                        'или он уже успешно произвел процедуру активации '
                                        'или по каким-то причинам был отключен')
        return email

    def send_vcode(self, request):
        Vcode(request).send(self.user, 'join')

Формы в Django позволяют подготавливать, принимать и обрабатывать данные от пользователей с последующим сохранением результатов.

Форма регистрации пользователя

JoinForm мы наследуем от класса ModelForm, это позволит нам построить форму по уже описанной модели User и немного сократить количество кода, например, использовать проверку на уникальность адреса электронной почты — автоматически.

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

В поле пароля мы описываем виджет для сокрытия ввода содержимого и отменяем автоматическое удаление пробелов в начале и в конце введенного значения.

Затем мы задаем порядок отображения полей формы.

В классе Meta мы указываем с какими полями модели мы хотели бы поработать.

Переходим к дополнительным проверкам полей и начнем с имени пользователя.

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

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

Например, можно использовать имя Анна-Мария, Елизавета Первая, R2-D2.

Второе регулярное выражение проверят не смешал ли пользователей алфавиты при вводе имени пользователя. Например, буквы "c" в обоих алфавитах выглядит одинаково, и при наборе имени человек может совершить ошибку. Или вовсе недобросовестный пользователь может попытаться выдать себя за другого человека.

Цель следующего регулярного выражения не допустить два и более пробелов или дефисов подряд. Например, дефис нужен для использования двойных имен, а пробел чтоб отделить имя от фамилии, но следующие примеры, уже злоупотребление или возможная попытка кражи личности:  Анна---Мария или Елизавета  Первая, в последнем случае наличие второго пробела между словами сложно определить на первый взгляд.

И последнее регулярное выражение призвано не допускать более двух пробелов или дефисов в имени. Например, Анна-Мария Первая, допустима, конструкция содержит двойное имя и фамилию, а вот Е-л-и-з-а-в-е-т-а — это уже злоупотребление.

Дополнительную проверку пароля осуществляем стандартными средствами Django, в файле настроек содержится список валидаторов, которые можно расширить или дополнить собственными проверками.

Например, по умолчанию это проверки на минимальную длину, принадлежность не только к цифрам, на совпадение с наиболее популярными паролями. И проверка на сходство пароля с другими полями пользователя, типа имени, адреса электронной почты и так далее.

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

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

Создаем список кодов от 100 до 999 простым циклом for и перемешиваем его в случайном порядке, через специальную функцию get_random, о ней чуть позже. Обходим список добавляя цифровой код к имени пользователя и одновременно проверяем не занят ли этот псевдоним в базе данных. В случае успеха сохраняем данный объект и отравляем письмо с просьбой подтвердить аккаунт. Вспомогательные объекты и функции Vcode и get_random мы опишем полностью в разделе Утилиты.

Если цикл завершился и уникальный псевдоним не был создан, значит имя очень сильно популярно, или его специально массово регистрируют. Подобные случаи лучше расследовать вручную и в случае необходимости принимать соответствующие меры. Например, чем больше аудитория проекта, тем больше потребуется диапазон уникальных значений. Наборы из тысяч или десятков тысяч кодов подойдут для самых крупных проектов с многомилионными регистрациями.

Форма повторной отправки подтверждения

JoinResendForm это обычная форма, которую необходимо будет вызвать в случае если пользователь не смог по каким-либо причинам потвердеть свой аккаунт.

В методе проверки адреса электронной почты, необходимо удостовериться, что пользователь существует и находиться в статусе ожидания подтверждения почты, а не заблокирован администрацией.

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