1. Модель покупателя
6 октября 2016 г. 12:47
Большинство веб-приложений отличают зарегистрированных пользователей от анонимного посетителя сайта, который рассматривается как несуществующий пользователь, и, следовательно, не ссылается на объект сессии или базы данных. Структура Django в этом плане не является исключением.
Этот подход хорош для веб-сайтов, которые работают на CMS (Content Management System), или для Блогов, где только избранной группе пользователей персонала должен быть разрешен доступ. Этот подход также работает для веб-сервисов, таких как социальные сети или интранет-приложения, где посетители должны пройти аутентификацию с самого начала своей сессии.
Но когда у вас интернет-магазин, то использование такого подхода имеет серьёзные недостатки. Обычно, посетитель, который начинает искать интересующие товары, добавляет их в свою корзину. Затем на этапе оформления заказа решает создавать пользовательский аккаунт, использовать существующий или продолжить как гость. Вот где всё становится сложнее.
Прежде всего, для неаутентифицированных посетителей сайта корзина никому не принадлежит. Но каждая корзина должна быть связана с его текущим посетителем сайта, поэтому общий анонимный объект пользователя не подходит для этой цели. К сожалению, фреймворк Django явно этого нет, но анонимный объект пользователя основан на назначенном session-Id.
Во-вторых, существует вариант, когда тележка превращается в порядок, но посетитель хочет продолжить в качестве гостя (при этом оставаясь анонимным). Объект заказа должен ссылаться на объект пользователя в базе данных. Такого рода пользователей будет рассматриваться как фейковые (фальшивки): не удастся войти в систему, сбросить пароль и т. д. Единственная информация, которая должна быть сохранена для такого фейкового пользователя, - это адрес электронной почты, в противном случае он не может быть проинформирован при изменении состояния его заказа.
Джанго в явном виде не допускает таких объектов пользователя в его модели базы данных. Но с помощью логического флага is_active, мы можем обмануть приложение, заставив интерпретировать такого гостевого посетителя как фальшивого анонимного пользователя.
Однако, так как такой подход является непереносимым во всех приложениях на основе Django, djangoSHOP вводит новую модель базы данных - модель Customer, которая расширяет существующую модель пользователя.
1.1. Свойства модели Покупателя (Customer model)
Модель Покупатель имеет связь 1:1 с существующей моделю User, а это значит, что для каждого клиента, всегда существует один и только один объект пользователя. Такой подход позволяет нам сделать несколько вещей.
Встроенная модель пользователя может быть выгружена и заменена на другую реализацию. Такая альтернативная реализация имеет небольшое ограничение. Она должна наследоваться от django.contrib.auth.models.AbstractBaseUser и от django.contrib.auth.models.PermissionMixin. Она должна также определить все поля, которые доступны в модели по умолчанию в django.contrib.auth.models.User.
Установив is_active флаг = False, мы можем создать гостей внутри модели User. Гости не могут войти в систему, они не могут сбросить пароль, и, следовательно, можно рассматривать их как "материализованных" анонимных пользователей.
Наличие гостей с записью в базе данных,дает нам еще одно преимущество: с помощью ключа сессии посетителя сайта в качестве имени пользователя объекта пользователя, можно установить связь между объектом пользователя в базе данных с иным анонимным посетителем. Это также позволяет моделям Корзину и Заказа всегда ссылаться к модели пользователя, так как они[модели Корзины и Заказа] не должны заботиться о том, аутентифицировался ли определенный пользователь или нет. Также поддерживается простой рабочий процесс, когда анонимный пользователь решает зарегистрироваться и аутентифицироваться в будущем.
1.2. Добавление модели Покупатель в наше приложение
Почти все модели в djangoSHOP, в том числе и сама модель Покупатель, использует Отложенный паттерн модели (Deferred Model Pattern). Это означает, что проект Django отвечает за материализацию этой модели и дополнительно позволяет продавцу добавлять произвольные поля для своей модели Покупатель. Например, варианты номер телефона, дату рождения, логическое поле "должен ли клиент получать рассылку новостей", его статус относительно скидок и т. д.
Самой простой способ материализовать данный класс Покупатель - сделать так, как сделано в наших моделях по умолчанию?
from shop.models.defaults.customer import Customer
Или если мы хотим дополнительные поля, тогда вместо строки выше мы должны создать собственную модель Покупатель:
from shop.models.customer import BaseCustomer class Customer(BaseCustomer): birth_date = models.DateField("Date of Birth") # other customer related fields
1.2.1. Настройка Middleware (промежуточного слоя)
Модель Покупатель создаётся автоматически каждый раз, когда посетитель заходит на сайт. Всякий раз, когда внутренний Django AuthenticationMiddleware добавляет AnonymousUser к объекту запроса, то CustomerMiddleware в djangoSHOP добавляет VisitingCustomer к объекту запроса. Ни AnonymousUser, ни VisitingCustomer не хранятся в базе данных.
Всякий раз, когда AuthenticationMiddleware добавляет подтверждение пользователя к объекту запроса, то CustomerMiddleware в djangoSHOP добавляет подтверждение клиента к объекту запроса. Если связанный Покупатель пока ещё не существует, то CustomerMiddleware его создает.
Поэтому добавьте CustomerMiddleware после AuthenticationMiddleware в settings.py:
MIDDLEWARE_CLASSES = ( ... 'django.contrib.auth.middleware.AuthenticationMiddleware', 'shop.middleware.CustomerMiddleware', ... )
1.2.2. Настройка контекст процессора (Context Processors)
Кроме того, некоторые шаблоны, возможно, потребуют доступ к объекту клиента через RequestContext. Поэтому добавьте этот контекст процессора к settings.py.
TEMPLATE_CONTEXT_PROCESSORS = ( ... 'shop.context_processors.customer', ... )
1.2.3. Детали реализации
Модель Покупатель имеет не нулевую 1:1 связь с моделью User. Поэтому каждый покупатель ассоциируется с одним конкретным пользователем. Например, доступ к хешированному паролю может быть получен через customer.user.password. Некоторые общие поля и методы из модели User, такие как first_name, last_name, email, is_anonymous() и is_authenticated() доступны напрямую, когда работаешь с объектом Покупатель. Сохранение объекта типа Покупатель также вызывает метод save() из ассоциированной модели User.
Другое направление - доступ к модели Покупатель из User - не всегда работает. Доступ к атрибуту не получится, если соответствующий объект покупателя потерян, другими словами, если не имеется обратной связи от Покупателя к данному объекту User.
>>> from django.contrib.auth import get_user_model >>> user = get_user_model().create(username='bobo') >>> print user.customer.salutation Traceback (most recent call last): File "<console>", line 1, in <module> File "django/db/models/fields/related.py", line 206, in __get__ self.related.get_accessor_name())) DoesNotExist: User has no customer.
Это может случится, когда объект User добавлен вручную или через другое Django приложение.
Во время запросов базы данных djangoSHOP всегда выполняет INNER JOIN между таблицей Customer и User. Поэтому лучше запросить пользователя через объект клиента, а не наоборот.
1.2.4. Анонимные пользователи и Посетители-покупатели
Большинство запросов на наш сайт будет содержать анонимный характер. Они не будут отправлять куки, содержащие session-id, и сервер не будет выделять специальное место под сессию. Милдварь добавляет объект VisitingCustomer в request, ассоциируя с объектом AnonymousUser. Эти два объекта не храняться внутри базы данных.
Всякий раз, когда анонимный пользователь/посетитель-покупатель добавляет свой первый товар в корзину, djangoSHOP конкретизирует объект пользователя в базе данных и связывает его с объектом клиента. Такой покупатель рассматриваестся как "незарегистрированный и invokingcustomer.is_authenticated() вернёт False; здесь связанная с ним модель Пользователь неактивен и не имеет пароль.
1.2.5. Гости и зарегистрированные пользователи
На шаге Оформления заявки, покупатель должен заявить о себе, хочет ли он продолжать как гость или войти под существующим аккаунтом, или зарегистрировать новый аккаунт. В первом случае (клиент желает продолжить в качестве гостя) объект User остается как есть: неактивный и без пароля. Во втором случае, посетитель входит с помощью Django аутентификационного бэкенда, который используется по умолчанию. В этот момент наполненная корзина объединяется с существующей корзиной пользователя. В последнем случае (клиент регистрируется) объект пользователь "обновляется", становясь активным Django Пользователем с паролем и адресом электронной почты.
1.2.6. Избавление критики
Некоторые скажут, что добавляя незарегистрированных и "гостевых" пользователей в таблицу пользователей в бд - это анти-паттерн или хак. Но какие есть альтернативы?
Мы могли бы хранить корзину или анонимных пользователей в хранилище сессий. Такой способ использовался в djangoSHOP до версии 0.2 (включая). Однако это вынуждало хранить две разные модели в корзине: одна для сессий, а другая для связи. Это очень не практично, особенно если модель корзины должна иметь возможность быть переопределённой собственной реализацией продавца.
Мы могли бы ассоциировать каждые модели корзины с session-id. Это могло бы потребовать дополнительное поле, которое было бы NULL для аутентифицированных покупателей. Хотя теоретически это возможно, но потребуется много кода, который выполняет различие между анонимными и аутентифицированными покупателями. Поскольку цель этого программного обеспечения - оставаться простым, поэтому эта идея была отклонена.
Мы могли бы хранить первичный ключ каждой корзины в сессии, связанной с анонимным пользователем/покупателем. Но было бы очень трудно найти корзины с истекшим сроком годности, потому что мы должны были бы перебрать все корзины и для каждой корзины мы должны перебрать все сеансы, чтобы проверить, соответствуют ли первичные ключи. Помните, что не существует такого понятия, как OUTER JOIN между сессиями и таблицами базы данных.
Мы могли бы создать объект Покупателя, который зависит от Пользователя. Так вместо наличия OneToOneField(AUTH_USER_MODEL) в модели Покупатель, мы имели бы 1:1 связь с внешним ключом, который может иметь NULL. Это требовало бы дополнительного поля в хранении session-id в моделе Покупатель. Это также требовало бы дополнительное поле email, если мы хотим оставлять "гостевых" покупателей как анонимных пользователей - что они, действительно, есть, только не могут аутентифицироваться. Помимо дублирования поля этот подход также потребует некоторого кода для того, чтобы различать непризнанных, "гостевых" и зарегистрированных клиентов. В дополнение к этому, бэкенд администрация потребует двух четко определенных взглядов, один для модели Покупатель и один для модели Пользователь.
1.3. Аутентификация по электронной почте
В настоящее время довольно часто используют адрес электронной почты для аутентификации вместо явного идентификатора учетной записи, такого как логин. В Django это не представляется возможным без замены встроенной модели Пользователь. Так как для интернет-магазина этот вариант аутентификации является весьма важным, с djangoSHOP поставляется дополнительное приложение, которое предназначено для замены встроенной модели Пользователь.
Эта модель пользователя практически идентична существующей модели пользователя, которую можно найти в django.contrib.auth.models.py. Разница заключается в том, что она использует поле Электронная почта, а не имя пользователя для поиска учетных данных. Чтобы активировать эту альтернативную модель пользователя, добавьте альтернативное приложение аутентификации settings.py проекта:
INSTALLED_APPS = ( 'django.contrib.auth', 'email_auth', ... )
Эта альтернативная модель Пользователь использует такую же таблицу auth_user в базе данных как использует модель Пользователь по умолчанию в Django. Поля альтернативной модели совместимы с встроенной модели и, следовательно, альтернативное приложение можно будет добавить позже к существующему проекту Django.
1.3.1. Оговорка при использовании альтернативной модели пользователя
Сообразительный читатель мог заметить, что в email_auth.models.User поле Электронная почта не объявлено как уникальное. Это, кстати, вызывает Джанго жаловаться во время запуска с:
WARNINGS: email_auth.User: (auth.W004) 'User.email' is named as the 'USERNAME_FIELD', but it is not unique. HINT: Ensure that your authentication backend(s) can handle non-unique usernames.
Это предупреждение можно отключить, сделав его молчаливым SILENCED_SYSTEM_CHECKS = ['auth.W004'] в settings.py.
И тому есть две причины:
Во-первых, по умолчанию Django модель Пользователь не имеет ограничений на уникальность значения поля электронной почты, поэтому email_auth остается более совместимыми.
Во-вторых, уникальность требуется только для пользователей, которые на самом деле могут войти в систему. Гостевые пользователи, с другой стороны, не могут войти в систему, но они могут когда-нибудь вернуться. Имея уникальное поле электронной почты, Django приложение email_auth будет блокировать их и гости будут иметь возможность купить только один раз, но не во второй раз - а этого мы, конечно, не хотим!
Поэтому djangoSHOP предлагает два настраиваемых параметра:
- Покупатели могут заявлять о себе в качестве гостей каждый раз, когда они что-то покупают. Это значение по умолчанию, но позволяет иметь неуникальные адреса электронной почты в базе данных.
- Покупатели могут заявить о себе в качестве гостей в первый раз, когда они что-то покупают. Если когда-нибудь они вернутся на сайт, чтобы купить второй раз, они будут признаны как вернувшиеся клиенты и должны будут использовать форму для сброса пароля. Эта конфигурация активируется установкой SHOP_GUEST_IS_ACTIVE_USER = True. Кроме того, она позволяет нам установить ограничение уникальности на поле электронной почты.
Замечание: Поле Электронная почта от встроенной пользовательской модели Django имеет максимальную длину 75 символов. Этого достаточно для большинства случаев использования, но нарушает RFC-5321, который требует 254 символов. Альтернативная реализация использует правильную максимальную длину.
1.3.2. Администрирование Пользователей и Покупателей
Для моделей Покупатель и Пользователь можно повторно использовать те же самые сгенерированные Django административные интерфейсы. Для этого нужно импортировать и зарегистрировать бэкенд пользователя внутри admin.py:
from django.contrib import admin from shop.admin.customer import CustomerProxy, CustomerAdmin admin.site.register(CustomerProxy, CustomerAdmin)
Бэкенд администратора перерабатывает встроенный django.contrib.auth.admin.UserAdmin, и дополняет его, добавив модель Покупателя как StackedInlineAdmin на верхней части страницы. Поступая таким образом, мы можем изменять поля покупателя и пользователя на той же странице.
1.4. Сводная информация для Покупателя и Пользователя
Данная таблица подводит различие между Django моделью Пользователь [1] и моделью Shop Покупатель:
Shop’s Customer Shop Покупатель |
Model Django’s User Model Django модель Пользователь |
Active Session Активация сессии |
---|---|---|
VisitingCustomer объект | AnonymousUser объект | Нет |
Непризнанный Покупатель | Неактивный User объект без пароля | Да, но не залогинен |
Покупатель признан как гость [2] | Неактивный User объект с валидной эл. почтой, но без пароля | Да, но не залогинен |
Покупатель признан как гость [3] | Активный User объект с непригодным паролем, но имеет возможность сбросить пароль | Да, но не залогинен |
Зарегистрированный Покупатель | Активный User с валидной эл. почтой, знающий пароль, приветствие (опционально), фамилию и имя и т. д. | Да, залогинен, с помощью Django бэкенда аутентификации |
[1] или другая альтернативная модель User, установленная в переменной AUTH_USER_MODEL.
[2] если SHOP_GUEST_IS_ACTIVE_USER = False (по умолчанию).
[3] если SHOP_GUEST_IS_ACTIVE_USER = True.
1.4.1. Управление Покупателями
DjangoSHOP поставляется со специальной командой, которая информирует продавца о состоянии покупателей. В папке проекта, вызовите в командной строке:
./manage.py shop_customers Customers in this shop: total=20482, anonymous=17418, expired=10111, active=1068, guests=1997, registered=1067, staff=5.
Читаются эти числа так:
- Анонимные покупатели - это те, кто добавил хотя бы один товар в корзину, но прошёл процесс оформления заявки.
- С истёкшим сроком покупатели являются подмножеством анонимных покупателей, чья сессия уже истекли.
- Различие между гостем и зарегистрированным покупателем объяснено в таблице выше.
1.4.1.1. Удаление с истёкшим сроком покупателей
Вызов команды в коммандной строке:
./manage.py shop_customers --delete-expired
Удаляет все анонимные/незарегистрированные покупатели и их связанные сущности с пользователем из базы данных, чьи сессии истекли. Эта команда может использоваться для уменьшения размера базы данных.
Представляю вашему вниманию книгу, написанную моим близким другом Максимом Макуриным: Секреты эффективного управления ассортиментом.
Книга предназначается для широкого круга читателей и, по мнению автора, будет полезна специалистам отдела закупок и логистики, категорийным и финансовым менеджерам, менеджерам по продажам, аналитикам, руководителям и директорам, в компетенции которых принятие решений по управлению ассортиментом.
Комментарии: 0