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

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

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

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

Сергей Серов

Настройки

Открываем и добавляем настройки в файл settings.py:

SITE_SSL = False
SITE_DOMAIN = '127.0.0.1:8000'

ACCOUNTS_VCODE_TIMEOUT = 60 * 5  # время жизни проверочного кода (в секундах)

DEFAULT_FROM_EMAIL = '{title} <{email}>'.format(title=SITE_TITLE, email='test@test.test')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

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

Далее мы задаем "время жизни" кода для подтверждения адреса электронной почты.

Затем мы указываем адрес, с которого мы будем отправлять всю корреспонденцию.

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

Утилиты

Для начала опишем утилиты, которые могут найти применение не только в приложении accounts, в директории core создаем utils.py:

import random
import hashlib
import time

from django.conf import settings


def get_random():
    try:  # по возможности используем системный рандом
        return random.SystemRandom()
    except NotImplementedError:
        random.seed(hashlib.sha256('{}{}{}'.format(
            random.getstate(), time.time(), settings.SECRET_KEY
        ).encode('utf-8')).digest())
        return random


def serializing_datetime(dt):
    dt = dt.isoformat()
    if dt.endswith('+00:00'):
        dt = dt[:-6] + 'Z'
    return dt


def get_domain():
    protocol = 'https' if settings.SITE_SSL else 'http'
    return '{}://{}'.format(protocol, settings.SITE_DOMAIN)

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

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

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

Переходим в каталог accounts и создаем файл utils.py:

import datetime

from django.utils.crypto import get_random_string
from django.utils import timezone
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.utils.dateparse import parse_datetime
from django.conf import settings

from core.utils import serializing_datetime, get_domain

from .models import User


class Vcode:
    def __init__(self, request):
        self.request = request
        self.vcode_name = 'accounts_vcode'
        self.user_pk = None
        self.data = None

    def send(self, user, action, data=None):
        vcode = get_random_string()
        self.request.session[self.vcode_name] = [vcode, action, data, user.pk, serializing_datetime(timezone.now())]
        subject = render_to_string('accounts/vcode/subject.txt')
        message = render_to_string('accounts/vcode/{action}.txt'.format(action=action), {
            'domain': get_domain(),
            'vcode': vcode,
        })
        addressee = data['email'] if action == 'change_email' else user.email
        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [addressee])

    def is_valid(self, code, action):
        if self.request.session.get(self.vcode_name, False):
            # получаем и удаляем вкод (одноразовый код)
            true_code, true_action, self.data, self.user_pk, created = self.request.session.pop(self.vcode_name)
            expiration_date = parse_datetime(created) + datetime.timedelta(seconds=settings.ACCOUNTS_VCODE_TIMEOUT)
            if code == true_code and action == true_action and expiration_date > timezone.now():
                return True

    def get_inactive_user(self):
        return User.inactive.get(pk=self.user_pk)

    def get_content(self):
        return User.active.get(pk=self.user_pk), self.data

Метод send принимает объект пользователя, название действия для подтверждения и дополнительные данные, например, новый адрес электронный почты.

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

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

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

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

Метод is_valid принимает код и название проверяемого действия. Получаем из сессии ранее сгенерированные данные путем изъятия. Код в виде ссылке подтверждения будет либо копироваться и вставляться в браузер, либо вызываться сразу из письма. Риск неправильного ввода кода ничтожен и скорее причина будет в "просрочке" проверочного кода, в таком случае пользователь сможет запросить ссылку для подтверждения заново.

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