Реализована трёхуровневая система статусов товаров и комплектов: - active (Активный) - товар доступен для продажи - archived (Архивный) - скрыт, можно восстановить в следующем сезоне - discontinued (Снят) - морально устарел, готов к удалению Изменения: 1. Модели (BaseProductEntity, Product, ProductKit): - Заменено поле is_deleted (Boolean) на status (CharField) - Добавлены архивные метаданные (archived_at, archived_by) - Обновлены методы: archive(), restore(), discontinue(), delete() - Уникальное ограничение изменено на conditional (status='active') 2. Менеджеры (ActiveManager, SoftDeleteQuerySet): - Полиморфная поддержка обеих систем (status и is_active) - Использует hasattr() для совместимости с наследниками - Методы: archive(), restore(), discontinue(), archived_only(), active_only() 3. Формы (ProductForm, ProductKitForm): - Включены поле status в формы - Валидация уникальности по status='active' - CSS классы для статус-селектора 4. Admin панель: - DeletedFilter переименован в StatusFilter с тремя опциями - get_status_display() с цветным отображением статуса - Actions: restore_items, hard_delete_selected, delete_selected - Readonly поля для архивирования 5. Представления: - ProductListView: фильтр status вместо is_active - CombinedProductListView: поддержка фильтра status для товаров и комплектов - API views обновлены для работы со статусом 6. Шаблоны: - product_form.html: form.status вместо form.is_active - productkit_create.html: form.status вместо form.is_active - productkit_edit.html: form.status вместо form.is_active 7. Миграции: - Удалены все старые миграции (чистый перезапуск по требованию пользователя) - Создана новая миграция 0001_initial с полной структурой status-системы - Удален старый код преобразования is_deleted -> status Проведённые проверки: - Django system check passed ✓ - Полиморфные менеджеры работают с обеими системами - Уникальные ограничения корректно работают с условиями - История заказов сохраняется даже после архивирования товара (django-simple-history) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
580 lines
25 KiB
Python
580 lines
25 KiB
Python
from django import forms
|
||
from django.forms import inlineformset_factory
|
||
from .models import Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem, ProductKitPhoto, ProductCategoryPhoto, ProductVariantGroup, ProductVariantGroupItem
|
||
|
||
|
||
class ProductForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования товара.
|
||
Поле photos НЕ включено в форму - файлы обрабатываются отдельно в view.
|
||
"""
|
||
categories = forms.ModelMultipleChoiceField(
|
||
queryset=ProductCategory.objects.filter(is_active=True),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Категории"
|
||
)
|
||
|
||
tags = forms.ModelMultipleChoiceField(
|
||
queryset=ProductTag.objects.filter(is_active=True),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Теги"
|
||
)
|
||
|
||
class Meta:
|
||
model = Product
|
||
fields = [
|
||
'name', 'sku', 'description', 'short_description', 'categories',
|
||
'tags', 'unit', 'cost_price', 'price', 'sale_price', 'status'
|
||
]
|
||
labels = {
|
||
'name': 'Название',
|
||
'sku': 'Артикул',
|
||
'description': 'Описание',
|
||
'short_description': 'Краткое описание',
|
||
'categories': 'Категории',
|
||
'tags': 'Теги',
|
||
'unit': 'Единица измерения',
|
||
'cost_price': 'Себестоимость',
|
||
'price': 'Основная цена',
|
||
'sale_price': 'Цена со скидкой',
|
||
'status': 'Статус'
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Make fields more user-friendly
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control form-control-lg fw-semibold',
|
||
'placeholder': 'Введите название товара'
|
||
})
|
||
self.fields['sku'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'Артикул (необязательно, будет сгенерирован автоматически)'
|
||
})
|
||
self.fields['description'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'rows': 3
|
||
})
|
||
self.fields['short_description'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'rows': 2,
|
||
'placeholder': 'Краткое описание для превью и площадок'
|
||
})
|
||
self.fields['cost_price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['sale_price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['unit'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['status'].widget.attrs.update({'class': 'form-control'})
|
||
|
||
def clean(self):
|
||
"""Валидация уникальности имени для активных товаров"""
|
||
cleaned_data = super().clean()
|
||
name = cleaned_data.get('name')
|
||
|
||
if name:
|
||
# Проверяем уникальность имени среди активных товаров
|
||
# Исключаем текущий товар при редактировании (self.instance.pk)
|
||
existing = Product.objects.filter(
|
||
name=name,
|
||
status='active'
|
||
)
|
||
|
||
if self.instance.pk:
|
||
existing = existing.exclude(pk=self.instance.pk)
|
||
|
||
if existing.exists():
|
||
self.add_error('name',
|
||
f'Товар с названием "{name}" уже существует. '
|
||
f'Пожалуйста, используйте другое название.'
|
||
)
|
||
|
||
return cleaned_data
|
||
|
||
|
||
class ProductKitForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования комплекта.
|
||
Цена комплекта вычисляется автоматически из цен компонентов.
|
||
"""
|
||
categories = forms.ModelMultipleChoiceField(
|
||
queryset=ProductCategory.objects.filter(is_active=True),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Категории"
|
||
)
|
||
|
||
tags = forms.ModelMultipleChoiceField(
|
||
queryset=ProductTag.objects.filter(is_active=True),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Теги"
|
||
)
|
||
|
||
class Meta:
|
||
model = ProductKit
|
||
fields = [
|
||
'name', 'sku', 'description', 'short_description', 'categories',
|
||
'tags', 'sale_price', 'price_adjustment_type', 'price_adjustment_value', 'status'
|
||
]
|
||
labels = {
|
||
'name': 'Название',
|
||
'sku': 'Артикул',
|
||
'description': 'Описание',
|
||
'short_description': 'Краткое описание',
|
||
'categories': 'Категории',
|
||
'tags': 'Теги',
|
||
'sale_price': 'Цена со скидкой',
|
||
'price_adjustment_type': 'Как изменить итоговую цену',
|
||
'price_adjustment_value': 'Значение корректировки',
|
||
'status': 'Статус'
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Make fields more user-friendly
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'Введите название комплекта'
|
||
})
|
||
self.fields['sku'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'Артикул (необязательно, будет сгенерирован автоматически)'
|
||
})
|
||
self.fields['description'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'rows': 3
|
||
})
|
||
self.fields['short_description'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'rows': 2,
|
||
'placeholder': 'Краткое описание для превью и площадок'
|
||
})
|
||
self.fields['sale_price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['price_adjustment_type'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['price_adjustment_value'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'step': '0.01',
|
||
'placeholder': '0'
|
||
})
|
||
self.fields['status'].widget.attrs.update({'class': 'form-control'})
|
||
|
||
def clean(self):
|
||
"""
|
||
Валидация формы комплекта.
|
||
Проверяет:
|
||
1. Уникальность имени для активных комплектов
|
||
2. Что если выбран тип корректировки, указано значение
|
||
"""
|
||
cleaned_data = super().clean()
|
||
|
||
# Проверяем уникальность имени среди активных комплектов
|
||
name = cleaned_data.get('name')
|
||
if name:
|
||
existing = ProductKit.objects.filter(
|
||
name=name,
|
||
status='active',
|
||
is_temporary=False
|
||
)
|
||
|
||
if self.instance.pk:
|
||
existing = existing.exclude(pk=self.instance.pk)
|
||
|
||
if existing.exists():
|
||
self.add_error('name',
|
||
f'Комплект с названием "{name}" уже существует. '
|
||
f'Пожалуйста, используйте другое название.'
|
||
)
|
||
|
||
adjustment_type = cleaned_data.get('price_adjustment_type')
|
||
adjustment_value = cleaned_data.get('price_adjustment_value')
|
||
|
||
# Если выбран тип корректировки (не 'none'), значение обязательно
|
||
if adjustment_type and adjustment_type != 'none':
|
||
if not adjustment_value or adjustment_value == 0:
|
||
raise forms.ValidationError(
|
||
'Укажите значение корректировки цены (> 0)'
|
||
)
|
||
|
||
return cleaned_data
|
||
|
||
|
||
class KitItemForm(forms.ModelForm):
|
||
"""
|
||
Форма для одного компонента комплекта.
|
||
Валидирует, что указан либо product, либо variant_group (но не оба).
|
||
Если обе поля пусты - это пустая форма, которая будет удалена.
|
||
"""
|
||
class Meta:
|
||
model = KitItem
|
||
fields = ['product', 'variant_group', 'quantity']
|
||
labels = {
|
||
'product': 'Конкретный товар',
|
||
'variant_group': 'Группа вариантов',
|
||
'quantity': 'Количество'
|
||
}
|
||
widgets = {
|
||
'product': forms.Select(attrs={'class': 'form-control'}),
|
||
'variant_group': forms.Select(attrs={'class': 'form-control'}),
|
||
'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'min': '0'}),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Устанавливаем значение по умолчанию для quantity = 1
|
||
if not self.instance.pk: # Только для новых форм (создание, не редактирование)
|
||
self.fields['quantity'].initial = 1
|
||
|
||
def clean(self):
|
||
cleaned_data = super().clean()
|
||
product = cleaned_data.get('product')
|
||
variant_group = cleaned_data.get('variant_group')
|
||
quantity = cleaned_data.get('quantity')
|
||
|
||
# Если оба поля пусты - это пустая форма (не валидируем, она будет удалена)
|
||
if not product and not variant_group:
|
||
# Для пустых форм обнуляем количество
|
||
cleaned_data['quantity'] = None
|
||
return cleaned_data
|
||
|
||
# Валидация: должен быть указан либо product, либо variant_group (но не оба)
|
||
if product and variant_group:
|
||
raise forms.ValidationError(
|
||
"Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно."
|
||
)
|
||
|
||
# Валидация: если выбран товар/группа, количество обязательно и должно быть > 0
|
||
if (product or variant_group):
|
||
if not quantity or quantity <= 0:
|
||
raise forms.ValidationError('Необходимо указать количество больше 0')
|
||
|
||
return cleaned_data
|
||
|
||
|
||
# Кастомный базовый формсет с валидацией на дубликаты
|
||
class BaseKitItemFormSet(forms.BaseInlineFormSet):
|
||
def clean(self):
|
||
"""Проверка на дубликаты товаров в комплекте"""
|
||
if any(self.errors):
|
||
# Не проверяем дубликаты если есть другие ошибки
|
||
return
|
||
|
||
products = []
|
||
variant_groups = []
|
||
|
||
for form in self.forms:
|
||
if self.can_delete and self._should_delete_form(form):
|
||
continue
|
||
|
||
product = form.cleaned_data.get('product')
|
||
variant_group = form.cleaned_data.get('variant_group')
|
||
|
||
# Проверка дубликатов товаров
|
||
if product:
|
||
if product in products:
|
||
raise forms.ValidationError(
|
||
f'Товар "{product.name}" добавлен в комплект более одного раза. '
|
||
f'Каждый товар может быть добавлен только один раз.'
|
||
)
|
||
products.append(product)
|
||
|
||
# Проверка дубликатов групп вариантов
|
||
if variant_group:
|
||
if variant_group in variant_groups:
|
||
raise forms.ValidationError(
|
||
f'Группа вариантов "{variant_group.name}" добавлена более одного раза. '
|
||
f'Каждая группа может быть добавлена только один раз.'
|
||
)
|
||
variant_groups.append(variant_group)
|
||
|
||
# Формсет для создания комплектов (с пустой формой для удобства)
|
||
KitItemFormSetCreate = inlineformset_factory(
|
||
ProductKit,
|
||
KitItem,
|
||
form=KitItemForm,
|
||
formset=BaseKitItemFormSet,
|
||
fields=['product', 'variant_group', 'quantity'],
|
||
extra=1, # Показать 1 пустую форму для первого компонента
|
||
can_delete=True, # Разрешить удаление компонентов
|
||
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
|
||
validate_min=False, # Не требовать минимум компонентов
|
||
can_delete_extra=True, # Разрешить удалять дополнительные формы
|
||
)
|
||
|
||
# Формсет для редактирования комплектов (без пустых форм, только существующие компоненты)
|
||
KitItemFormSetUpdate = inlineformset_factory(
|
||
ProductKit,
|
||
KitItem,
|
||
form=KitItemForm,
|
||
formset=BaseKitItemFormSet,
|
||
fields=['product', 'variant_group', 'quantity'],
|
||
extra=0, # НЕ показывать пустые формы при редактировании
|
||
can_delete=True, # Разрешить удаление компонентов
|
||
min_num=0, # Минимум 0 компонентов
|
||
validate_min=False, # Не требовать минимум компонентов
|
||
can_delete_extra=True, # Разрешить удалять дополнительные формы
|
||
)
|
||
|
||
# Для обратной совместимости (если где-то еще используется KitItemFormSet)
|
||
KitItemFormSet = KitItemFormSetCreate
|
||
|
||
|
||
class ProductCategoryForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования категории товаров.
|
||
Поле photos НЕ включено в форму - файлы обрабатываются отдельно в view.
|
||
"""
|
||
parent = forms.ModelChoiceField(
|
||
queryset=ProductCategory.objects.filter(is_active=True),
|
||
required=False,
|
||
empty_label="Нет (корневая категория)",
|
||
label="Родительская категория",
|
||
widget=forms.Select(attrs={'class': 'form-control'})
|
||
)
|
||
|
||
class Meta:
|
||
model = ProductCategory
|
||
fields = ['name', 'sku', 'slug', 'parent', 'is_active']
|
||
labels = {
|
||
'name': 'Название',
|
||
'sku': 'Артикул',
|
||
'slug': 'URL-идентификатор',
|
||
'parent': 'Родительская категория',
|
||
'is_active': 'Активна'
|
||
}
|
||
help_texts = {
|
||
'sku': 'Оставьте пустым для автоматической генерации (CAT-XXXX)',
|
||
'slug': 'Оставьте пустым для автоматической генерации из названия',
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Make fields more user-friendly
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control form-control-lg fw-semibold',
|
||
'placeholder': 'Введите название категории'
|
||
})
|
||
self.fields['sku'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'CAT-XXXX (автоматически)'
|
||
})
|
||
self.fields['slug'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'url-identifier (автоматически)'
|
||
})
|
||
self.fields['slug'].required = False # Делаем поле необязательным
|
||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||
|
||
# Исключаем текущую категорию и её потомков из списка родительских
|
||
# (чтобы не создать циклическую зависимость)
|
||
if self.instance and self.instance.pk:
|
||
# Получаем все потомки текущей категории
|
||
descendants = self._get_descendants(self.instance)
|
||
# Исключаем текущую категорию и все её потомки
|
||
exclude_ids = [self.instance.pk] + [cat.pk for cat in descendants]
|
||
self.fields['parent'].queryset = ProductCategory.objects.filter(
|
||
is_active=True
|
||
).exclude(pk__in=exclude_ids)
|
||
|
||
def clean(self):
|
||
"""Валидация уникальности имени для активных категорий"""
|
||
cleaned_data = super().clean()
|
||
name = cleaned_data.get('name')
|
||
|
||
if name:
|
||
# Проверяем уникальность имени среди активных категорий
|
||
existing = ProductCategory.objects.filter(
|
||
name=name,
|
||
is_deleted=False
|
||
)
|
||
|
||
if self.instance.pk:
|
||
existing = existing.exclude(pk=self.instance.pk)
|
||
|
||
if existing.exists():
|
||
self.add_error('name',
|
||
f'Категория с названием "{name}" уже существует. '
|
||
f'Пожалуйста, используйте другое название.'
|
||
)
|
||
|
||
return cleaned_data
|
||
|
||
def clean_slug(self):
|
||
"""Преобразуем пустую строку в None для автогенерации slug"""
|
||
slug = self.cleaned_data.get('slug')
|
||
if slug == '' or slug is None:
|
||
return None
|
||
return slug
|
||
|
||
def _get_descendants(self, category):
|
||
"""Рекурсивно получает всех потомков категории"""
|
||
descendants = []
|
||
children = category.children.all()
|
||
for child in children:
|
||
descendants.append(child)
|
||
descendants.extend(self._get_descendants(child))
|
||
return descendants
|
||
|
||
|
||
# ==================== VARIANT GROUP FORMS ====================
|
||
|
||
class ProductVariantGroupForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования группы вариантов товара.
|
||
"""
|
||
class Meta:
|
||
model = ProductVariantGroup
|
||
fields = ['name', 'description']
|
||
labels = {
|
||
'name': 'Название группы',
|
||
'description': 'Описание (для какого букета или ситуации создана)'
|
||
}
|
||
widgets = {
|
||
'name': forms.TextInput(attrs={
|
||
'class': 'form-control form-control-lg fw-semibold',
|
||
'placeholder': 'Например: Роза красная Freedom'
|
||
}),
|
||
'description': forms.Textarea(attrs={
|
||
'class': 'form-control',
|
||
'rows': 3,
|
||
'placeholder': 'Описание группы и её использования'
|
||
}),
|
||
}
|
||
|
||
|
||
class ProductVariantGroupItemForm(forms.ModelForm):
|
||
"""
|
||
Форма для добавления товара в группу с приоритетом.
|
||
"""
|
||
class Meta:
|
||
model = ProductVariantGroupItem
|
||
fields = ['product', 'priority']
|
||
labels = {
|
||
'product': 'Товар',
|
||
'priority': 'Приоритет'
|
||
}
|
||
widgets = {
|
||
'product': forms.Select(attrs={'class': 'form-control form-control-sm'}),
|
||
'priority': forms.NumberInput(attrs={
|
||
'class': 'form-control form-control-sm',
|
||
'min': '1',
|
||
'placeholder': '1, 2, 3...'
|
||
}),
|
||
}
|
||
|
||
|
||
# Базовый формсет с валидацией на дубликаты
|
||
class BaseProductVariantGroupItemFormSet(forms.BaseInlineFormSet):
|
||
def clean(self):
|
||
"""Проверка на дубликаты товаров в группе"""
|
||
if any(self.errors):
|
||
return
|
||
|
||
products = []
|
||
|
||
for form in self.forms:
|
||
if self.can_delete and self._should_delete_form(form):
|
||
continue
|
||
|
||
product = form.cleaned_data.get('product')
|
||
|
||
# Проверка дубликатов товаров
|
||
if product:
|
||
if product in products:
|
||
raise forms.ValidationError(
|
||
f'Товар "{product.name}" уже добавлен в группу. '
|
||
f'Каждый товар может быть добавлен только один раз.'
|
||
)
|
||
products.append(product)
|
||
|
||
|
||
# Формсет для создания групп (с пустой формой для добавления товара)
|
||
ProductVariantGroupItemFormSetCreate = inlineformset_factory(
|
||
ProductVariantGroup,
|
||
ProductVariantGroupItem,
|
||
form=ProductVariantGroupItemForm,
|
||
formset=BaseProductVariantGroupItemFormSet,
|
||
fields=['product', 'priority'],
|
||
extra=1, # Показать 1 пустую форму для первого товара
|
||
can_delete=True, # Разрешить удаление товаров
|
||
min_num=0, # Минимум 0 товаров (можно создать пустую группу)
|
||
validate_min=False, # Не требовать минимум товаров
|
||
can_delete_extra=True, # Разрешить удалять дополнительные формы
|
||
)
|
||
|
||
# Формсет для редактирования групп (без пустых форм, только существующие товары)
|
||
ProductVariantGroupItemFormSetUpdate = inlineformset_factory(
|
||
ProductVariantGroup,
|
||
ProductVariantGroupItem,
|
||
form=ProductVariantGroupItemForm,
|
||
formset=BaseProductVariantGroupItemFormSet,
|
||
fields=['product', 'priority'],
|
||
extra=0, # НЕ показывать пустые формы при редактировании
|
||
can_delete=True, # Разрешить удаление товаров
|
||
min_num=0, # Минимум 0 товаров
|
||
validate_min=False, # Не требовать минимум товаров
|
||
can_delete_extra=True, # Разрешить удалять дополнительные формы
|
||
)
|
||
|
||
|
||
class ProductTagForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования тегов товаров.
|
||
"""
|
||
class Meta:
|
||
model = ProductTag
|
||
fields = ['name', 'slug', 'is_active']
|
||
labels = {
|
||
'name': 'Название',
|
||
'slug': 'URL-идентификатор',
|
||
'is_active': 'Активен'
|
||
}
|
||
help_texts = {
|
||
'slug': 'Оставьте пустым для автоматической генерации из названия',
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control form-control-lg fw-semibold',
|
||
'placeholder': 'Введите название тега'
|
||
})
|
||
self.fields['slug'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'url-identifier (автоматически)'
|
||
})
|
||
self.fields['slug'].required = False
|
||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||
|
||
def clean(self):
|
||
"""Валидация уникальности имени для активных тегов"""
|
||
cleaned_data = super().clean()
|
||
name = cleaned_data.get('name')
|
||
is_active = cleaned_data.get('is_active', True)
|
||
|
||
if name and is_active:
|
||
# Проверяем уникальность имени среди активных тегов
|
||
existing = ProductTag.objects.filter(
|
||
name=name,
|
||
is_active=True
|
||
)
|
||
|
||
if self.instance.pk:
|
||
existing = existing.exclude(pk=self.instance.pk)
|
||
|
||
if existing.exists():
|
||
self.add_error('name',
|
||
f'Тег с названием "{name}" уже существует. '
|
||
f'Пожалуйста, используйте другое название.'
|
||
)
|
||
|
||
return cleaned_data
|
||
|
||
def clean_slug(self):
|
||
"""Разрешаем пустой slug - он сгенерируется в модели"""
|
||
slug = self.cleaned_data.get('slug')
|
||
if slug == '' or slug is None:
|
||
return None
|
||
return slug
|