mod: minor: валидатор форм, парсера и моделей (28) миграции и переименование поля s_offer_skip32 в s_offer_code в модели TbOffer

This commit is contained in:
2026-07-15 17:20:54 +03:00
parent a2986cf719
commit 9ba5ef2af6
4 changed files with 113 additions and 29 deletions
+46 -26
View File
@@ -148,7 +148,7 @@
# │ f_offer_price │ Цена [indexed для сортировки]
# │ i_offer_quantity │ Количество в наличии [indexed]
# │ i_offer_discount_to_daily_sale │ % скидка [indexed для фильтров]
# │ s_offer_skip32 │ Хеш для корзины (unique)
# │ s_offer_code │ Хеш для корзины (unique)
# │ i_offer_views │ Счетчик просмотров
# │ i_offer_favorites │ Счетчик в избранном
# │ t_offer_created │ Timestamp
@@ -267,6 +267,34 @@ from lpon_site.settings import (
logger = logging.getLogger(__name__)
# ============================================================================
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ DEFAULTS В МОДЕЛЯХ
# ============================================================================
def get_image_metadata_default():
"""
Возвращает значение по умолчанию для j_img_metadata.
Используется как callable default в Django моделях (Django не может сериализовать lambda).
"""
return {
KEY_IMAGE_TYPE: VALUE_IMAGE_ABSTRACT,
KEY_IMAGE_FROM: VALUE_IMAGE_FROM_USER,
KEY_IMAGE_URL: None,
KEY_IMAGE_NOTE: '???',
}
def get_offer_metadata_default():
"""
Возвращает значение по умолчанию для j_offer_metadata.
Используется как callable default в Django моделях (Django не может сериализовать lambda).
"""
return {
KEY_OFFER_NOTE: '',
KEY_OFFER_ALL_MEDIA: [],
}
# ============================================================================
# МЕТАДАННЫЕ ИЗОБРАЖЕНИЙ
# ============================================================================
@@ -312,12 +340,7 @@ class TbImageMetadata(models.Model):
j_img_metadata = models.JSONField(
# Гибкие дополнительные данные об изображении с предзаполненной структурой
# Пример: {"IMG_IS": "abstract", "IMG_FROM": "ivan", "IMG_URL": None, "IMG_NOTE": "обложка"}
default=lambda: {
KEY_IMAGE_TYPE: VALUE_IMAGE_ABSTRACT,
KEY_IMAGE_FROM: VALUE_IMAGE_FROM_USER,
KEY_IMAGE_URL: None,
KEY_IMAGE_NOTE: '???', # Автогенерация через парсер или админку
},
default=get_image_metadata_default,
blank=True,
null=False, # Изменено с True на False т.к. теперь всегда есть структура default
verbose_name='Метаданные',
@@ -1287,10 +1310,7 @@ class TbOffer(models.Model):
)
j_offer_metadata = models.JSONField(
# Метаданные оффера (сырые данные из источника, координаты в Excel и т.д.)
default=lambda: {
KEY_OFFER_NOTE: '',
KEY_OFFER_ALL_MEDIA: [],
},
default=get_offer_metadata_default,
null=True,
verbose_name='Дополнительные данные',
help_text='Дополнительные данные о предложении в виде JSON-словаря. Например:<pre style=\"'
@@ -1308,13 +1328,13 @@ class TbOffer(models.Model):
' \"tp\" — Tape Reel Record, \"ur\" — Used Tape Reel (для записи),'
' \"??\" — Other</li></ul>',
)
s_offer_skip32 = models.CharField(
s_offer_code = models.CharField(
max_length=12,
unique=True,
verbose_name='Код товара',
help_text='Уникальный код товара для идентификации в корзине и при заказе (чтобы не светить id).'
' Например: "4gfFCJ". Формируется автоматически связкой Skip32 (хаотичное перемешивание) и'
' Base62 (компактная упаковка) из id оффера в методе save().',
' Например: "4gfFCJ". Формируется автоматически через Hashids на основе ID оффера.'
' <b>Не редактировать вручную!</b>',
)
i_offer_views = models.IntegerField(
default=0,
@@ -1332,7 +1352,7 @@ class TbOffer(models.Model):
def __str__(self):
seller = self.k_offer_to_source.k_source_to_seller.s_seller if (self.k_offer_to_source
and self.k_offer_to_source.k_source_to_seller) else "?"
return (f"[{self.s_offer_skip32:0>6}]\u00A0«{self.s_offer}»"
return (f"[{self.s_offer_code:0>6}]\u00A0«{self.s_offer}»"
f"\u00A0item\u00A0«{self.k_offer_to_item_id}»"
f"\u00A0seller\u00A0«{seller}»")
@@ -1351,10 +1371,10 @@ class TbOffer(models.Model):
def save(self, *args, **kwargs):
"""
Переопределенный метод save для:
1. Автоматического формирования s_offer_skip32 (криптографический код)
1. Автоматического формирования s_offer_code (криптографический код)
2. Записи истории изменений цены и количества в TbOfferHistory
ЛОГИКА s_offer_skip32:
ЛОГИКА s_offer_code:
- Для новых оферов: генерируем код после получения ID
- Для старых оферов БЕЗ кода: кодируем существующий ID (миграция)
- Для старых оферов С кодом: не трогаем
@@ -1364,25 +1384,25 @@ class TbOffer(models.Model):
- При обновлении: если цена или количество изменилось → создаем новую запись
- Это позволяет отследить полную историю изменений
"""
# 1. Проверяем нужно ли генерировать s_offer_skip32:
# 1. Проверяем нужно ли генерировать s_offer_code:
# - ИЛИ это новый объект (self.pk == None)
# - ИЛИ это старый объект без s_offer_skip32 (миграция)
if not self.pk or not self.s_offer_skip32:
# - ИЛИ это старый объект без s_offer_code (миграция)
if not self.pk or not self.s_offer_code:
# Если это новый оффер, сначала сохраняем, чтобы получить ID
if not self.pk:
# Сохраняем БЕЗ s_offer_skip32 чтобы Django создал запись и присвоил pk
# Сохраняем БЕЗ s_offer_code чтобы Django создал запись и присвоил pk
super().save(*args, **kwargs)
# После save() Django автоматически заполнит self.pk
# Кодируем pk в компактный, необратимый код
# Пример: pk=42 → "QBErd8"
self.s_offer_skip32 = Hashids(salt=OFFER_HASHIDS_SALT, min_length=OFFER_HASHIDS_MIN_LENGTH).encode(self.pk)
self.s_offer_code = Hashids(salt=OFFER_HASHIDS_SALT, min_length=OFFER_HASHIDS_MIN_LENGTH).encode(self.pk)
# Сохраняем только поле s_offer_skip32 (не перезаписываем остальное)
super().save(update_fields=['s_offer_skip32'])
# Сохраняем только поле s_offer_code (не перезаписываем остальное)
super().save(update_fields=['s_offer_code'])
else:
# Оффер существует И уже имеет s_offer_skip32: сохраняем как обычно
# Не трогаем s_offer_skip32, он был сформирован при создании
# Оффер существует И уже имеет s_offer_code: сохраняем как обычно
# Не трогаем s_offer_code, он был сформирован при создании
super().save(*args, **kwargs)
# 2. Записывает историю изменений цены и количества в TbOfferHistory.