4. Продуктовые модели

15 октября 2016 г. 10:16

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

4.1. Описание продуктов с помощью кастомной модели

DjangoSHOP требует описать продукты вместо заранее подготовленных каких-то универсальных моделей.

И это правильный подход - ведь только мы знаем как лучше всего смоделировать наши продукты!

4.1.1. Решения для интернет-магазина, которые строятся по типу plug-and-play (возьми-и-примени), обычно используют один из этих анти-паттернов

Либо паттерны предлагают поле для каждого возможного варианта, или они используют паттерн Сущность-Аттрибут-Значение (Entity Attribute Value(EAV)) для добавления мета-данных для каждой нашей модели. По-началу выглядит достаточно легким для внедрения. Но оба подхода на самом деле громоздкие и имеют серьёзные недостатки. Они оба применяют различную "физическую схему" как способ хранения данных, но не "логическую схему", которая нужна для пользователей и самого приложения. И если, в скором времени понадобиться делать интеграцию с какой-нибудь ERP истемой (Enterprise Resource Planning), мы вынуждены будем реализовывать обратные преобразования данных.

4.1.2. В djangoSHOP физическое представление продукта всегда отражает его логическую структуру

Минимальный набор моделей - вот решение этой проблемы в djangoSHOP. Эти абстрактные модели являются обёрткой, обеспечивающей подкласс физических моделей. Таким образом, логическое представление продукта соответствует их физической. Более того, можно даже реализовать различные виды продукции, путем создания полиморфных подклассов из абстрактной базовой модели. Благодаря Django’s Object Relational Mapper, моделирование логического представления для набора продуктов вместе с бэкэндом административной частью осуществляется практически без усилий.

Поэтому базовый класс для моделирования продукта является заглушкой и содержит только ниже перечисленные поля:

created_at и updated_at типа timestamps; эти поля очевидны.

Логическое поле active, которое используется для отметки доступности продукта.

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

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

Название продукта должно быть реализовано как поле модели или как property метод и должны быть объявлены как product_name. Используйте метод для составных и переводимых имен, в противном случае используйте поле в модели с таким именем.

Цена продукта должна быть реализована как метод, объявленный как get_price(request), который принимает объект request. Это дает продавцу способность варьировать цену и/или валюту в зависимости от географического расположения, статуса покупателей, параметра user-agent у используемого браузера и т. д.

Опциональное, но настоятельно рекомендуемое поле - код продукта (артикул), объявленный как product_code. Он будет возвращать уникальный и независимый от языка идентификатор для каждого продукта. Это нужно для уникальной идентификации продукта. В большинстве случаев код продукта (артикул) реализуется самой моделью продукта, но в некоторых случаях может быть реализован вариант продукта. SmartPhone из демо-проекта как раз тот случай.

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

4.2. Пример: Smart-Phones

Есть достаточно много моделей смартфонов с различное оборудование. Все функции те же кроме встроенной памяти. Как описать такую модель?

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

Но смартфоны имеют различное оборудование, а именно встроенную память, объём которой влияет на стоимость и уникальный код продукта (артикул). Поэтому наши продуктовые модели содержат два класса, общую модель смартфона и конкрентный экземпляр этой модели.

Поэтому мы будем моделировать наши смартфоны, используя модель похожую на следующую:

from shop.models.product import BaseProductManager, BaseProduct
from shop.money import Money

class SmartPhoneModel(BaseProduct):
    product_name = models.CharField(max_length=255,
        verbose_name=_("Product Name"))
    slug = models.SlugField(verbose_name=_("Slug"))
    description = HTMLField(help_text=_("Detailed description."))
    manufacturer = models.ForeignKey(Manufacturer,
        verbose_name=_("Manufacturer"))
    screen_size = models.DecimalField(_("Screen size"),
        max_digits=4, decimal_places=2)
    # other fields to map the specification sheet

    objects = BaseProductManager()
    lookup_fields = ('product_name__icontains',)

    def get_price(request):
        aggregate = self.smartphone_set.aggregate(models.Min('unit_price'))
        return Money(aggregate['unit_price__min'])

class SmartPhone(models.Model):
    product_model = models.ForeignKey(SmartPhoneModel)
    product_code = models.CharField(_("Product code"),
        max_length=255, unique=True)
    unit_price = MoneyField(_("Unit price"))
    storage = models.PositiveIntegerField(_("Internal Storage"))

Давайте поближе рассмотрим эти классы. Поля модели говорят сами за себя. Но нужно отметить, что каждый продукт требует поле product_name. По-другому можно реализовать это поле как свойство.

Другой важный атрибут для каждого продукта - это класс ProductManager. Он должен наследоваться от BaseProductManager с добавлением некоторых методов для генерации специальных queryset-ов.

И наконец, атрибут lookup_fields содержит список или кортеж полей поиска. Это требуется для бэкенда в административной панели и используется в редакторе при поиске продуктов. Поскольку фреймворк не советует, какие поля должны быть использованы в том или ином продукте, мы должны дать некоторые советы.

Какждый продукт также требует метод get_price(request). Он должен вернуть цену товара с использованием одного из доступных типов Money.

4.2.1. Добавление мультиязычной поддержки

Добавление мультиязычную поддержку в существующий продукт происходит достаточно легко. Для достижения этой цели djangoSHOP использует приложение django-parler, которая обеспечивает переводы моделей без неприятных хаков. Все, что нам нужно сделать, это заменить ProductManager, который будет способен обрабатывать переводы:

class ProductQuerySet(TranslatableQuerySet, PolymorphicQuerySet):
    pass

class ProductManager(BaseProductManager, TranslatableManager):
    queryset_class = ProductQuerySet

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

class SmartPhoneModel(BaseProduct, TranslatableModel):
    # other field remain unchanged
    description = TranslatedField()

class ProductTranslation(TranslatedFieldsModel):
    master = models.ForeignKey(SmartPhoneModel, related_name='translations', null=True)
    description = HTMLField(help_text=_("Some more detailed description."))

    class Meta:
        unique_together = [('language_code', 'master')]

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

4.2.2. Добавление поддержки полиморфизма

Если кроме смартфонов мы хотим продавать кабели, бумагу или карты памяти, мы должны разделить наши продуктовые модели в общие и специальные части. Другими словами, мы должны отделить информационные требования каждого продукта, от специфической информации конкретного типа продукта. Скажем, в дополнение к смартфонам, мы должны хотеть продавать карты памяти. Во-первых, мы должны объявить модель общую модель Product, которая является базовым классом для SmartPhone и SmartCard:

class Product(BaseProduct, TranslatableModel):
    product_name = models.CharField(max_length=255, verbose_name=_("Product Name"))
    slug = models.SlugField(verbose_name=_("Slug"), unique=True)
    description = TranslatedField()

    objects = ProductManager()
    lookup_fields = ('product_name__icontains',)

Далее, мы должны только добавить специфические атрибуты в модели класса, полученных из Product:

class SmartPhoneModel(Product):
    manufacturer = models.ForeignKey(Manufacturer, verbose_name=_("Manufacturer"))
    screen_size = models.DecimalField(_("Screen size"), max_digits=4, decimal_places=2)
    battery_type = models.PositiveSmallIntegerField(_("Battery type"), choices=BATTERY_TYPES)
    battery_capacity = models.PositiveIntegerField(help_text=_("Battery capacity in mAh"))
    ram_storage = models.PositiveIntegerField(help_text=_("RAM storage in MB"))
    # and many more attributes as found on the data sheet

class SmartPhone(models.Model):
    product_model = models.ForeignKey(SmartPhoneModel)
    product_code = models.CharField(_("Product code"), max_length=255, unique=True)
    unit_price = MoneyField(_("Unit price"))
    storage = models.PositiveIntegerField(_("Internal Storage"))

class SmartCard(Product):
    product_code = models.CharField(_("Product code"), max_length=255, unique=True)
    storage = models.PositiveIntegerField(help_text=_("Storage capacity in GB"))
    unit_price = MoneyField(_("Unit price"))
    CARD_TYPE = (2 * ('{}{}'.format(s, t),) for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro '))
    card_type = models.CharField(_("Card Type"), choices=CARD_TYPE, max_length=15)
    SPEED = ((str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280))
    speed = models.CharField(_("Transfer Speed"), choices=SPEED, max_length=8)

Если MyShop будет продавать iPhone5 с 16GB и 32GB памятью как независимый продукт, то мы могли бы объединить классы SmartPhoneModel и SmartPhone и переместить атрибуты product_code и unit_price в класс продукта. Это позволило бы упростить некоторые аспекты программирования, но потребует от торговца два раза добавлять одну и туже достаточно объёмную информации о продукте. Поэтому мы рекомендуем выше представленную модель компоновки.

4.3. Предостережение об использовании ManyToManyField в существующих моделях

Иногда мы можем нуждаться в использовании ManyToManyField для моделей, которые обрабатываются другими приложениями в нашем проекте. Это, например, может быть файлы атрибутов со ссылкой на модель filer.FilerFileField из библиотеки django-filer. Здесь Django будет пытаться создать таблицу сопоставления, где внешний ключ к нашей модели продукта не может работать должным образом, потому что во время разворачивания приложения, наша модель продукта считается отложенной.

Поэтому, мы вынуждены создать нашу собственную модель сопоставления и ссылаться на неё, используя параметр through, как показано на примере:

from six import with_metaclass
from django.db import models
from filer.fields.file import FilerFileField
from shop.models import deferred
from shop.models.product import BaseProductManager, BaseProduct

class ProductFile(with_metaclass(deferred.ForeignKeyBuilder, models.Model)):
    file = FilerFileField()
    product = deferred.ForeignKey(BaseProduct)

class Product(BaseProduct):
    # other fields
    files = models.ManyToManyField('filer.File', through=ProductFile)

    objects = ProductManager()

Замечание: не используй этот пример для создания поля many-to-many к FilerImageField. Вместо этого используй shop.models.related.BaseProductImage, который является базовым классом для такого рода отображения. Только импортируй и материализуй его в свой проект.

Оцените статью

5 из 5 (всего 1 оценка)

Поля, отмеченные звёздочкой ( * ) , являются обязательными.

Спасибо за ваш отзыв!

Автор перевода

Права на использование материала, расположенного на этой странице http://vivazzi.ru/django-shop/product-models/:

Разрешается копировать материал с указанием её автора и ссылки на оригинал без использования параметра rel="nofollow" в теге <a>. Использование:

Автор перевода: Мальцев Артём
Ссылка на перевод статьи: <a href="http://vivazzi.ru/django-shop/product-models/">http://vivazzi.ru/django-shop/product-models/</a>

Подробнее: Правила использования сайта

Комментариев: 0

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

Чтобы оставить комментарий от своего имени войдите или зарегистрируйтесь обычным способом или через социальные сети:

Отправить

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

Попробуйте