new model logic
This commit is contained in:
parent
b22fcd0d4d
commit
93636efecf
|
@ -0,0 +1,431 @@
|
|||
from django.contrib import admin
|
||||
from .models import (
|
||||
CustomRequest,
|
||||
FooterModel,
|
||||
LetterTemplate,
|
||||
Page,
|
||||
ReviewModel,
|
||||
Seo,
|
||||
SocialNetwork,
|
||||
Menu,
|
||||
Ref1C,
|
||||
)
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(Seo)
|
||||
class SeoAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Seo.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"id",
|
||||
"text",
|
||||
"email",
|
||||
"has_image", # Кастомное поле для проверки наличия изображения
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"text",
|
||||
"email",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("email",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"text",
|
||||
"email",
|
||||
"blank",
|
||||
"image",
|
||||
)
|
||||
|
||||
@admin.display(description="Есть изображение?", boolean=True)
|
||||
def has_image(self, obj):
|
||||
"""
|
||||
Проверяет, загружено ли изображение для записи.
|
||||
"""
|
||||
return bool(obj.image)
|
||||
|
||||
@admin.register(Page)
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Page.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"order",
|
||||
"menu_title",
|
||||
"title",
|
||||
"slug_preview", # Кастомное поле для предпросмотра slug
|
||||
"has_image", # Кастомное поле для проверки наличия изображения
|
||||
"external_link", # Внешняя ссылка
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"menu_title",
|
||||
"title",
|
||||
"slug",
|
||||
"content",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("order",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": ("order", "menu_title", "title", "slug"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Содержимое",
|
||||
{
|
||||
"fields": ("content", "content_rendered"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Медиа и ссылки",
|
||||
{
|
||||
"fields": ("image", "external_link"),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ("content_rendered",)
|
||||
|
||||
@admin.display(description="Slug (предпросмотр)", empty_value="—")
|
||||
def slug_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр slug.
|
||||
"""
|
||||
return obj.slug[:50] + "..." if obj.slug and len(obj.slug) > 50 else obj.slug
|
||||
|
||||
@admin.display(description="Есть изображение?", boolean=True)
|
||||
def has_image(self, obj):
|
||||
"""
|
||||
Проверяет, загружено ли изображение для записи.
|
||||
"""
|
||||
return bool(obj.image)
|
||||
|
||||
|
||||
@admin.register(Menu)
|
||||
class MenuAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Menu.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"type_display", # Кастомное поле для отображения типа меню
|
||||
"pages_count", # Кастомное поле для подсчета страниц в меню
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("type",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
filter_horizontal = ("pages",) # Удобный виджет для выбора страниц
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ()
|
||||
|
||||
@admin.display(description="Тип меню")
|
||||
def type_display(self, obj):
|
||||
"""
|
||||
Отображает читаемое название типа меню.
|
||||
"""
|
||||
return dict(obj._meta.get_field("type").choices).get(obj.type, "Неизвестно")
|
||||
|
||||
@admin.display(description="Количество страниц")
|
||||
def pages_count(self, obj):
|
||||
"""
|
||||
Подсчитывает количество страниц, связанных с меню.
|
||||
"""
|
||||
return obj.pages.count()
|
||||
|
||||
|
||||
@admin.register(ReviewModel)
|
||||
class ReviewModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели ReviewModel.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"text_preview", # Кастомное поле для предпросмотра текста отзыва
|
||||
"comment_preview", # Кастомное поле для предпросмотра комментария
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"text",
|
||||
"comment",
|
||||
)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"image",
|
||||
"text",
|
||||
"comment",
|
||||
)
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ()
|
||||
|
||||
@admin.display(description="Текст (предпросмотр)", empty_value="—")
|
||||
def text_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр текста отзыва.
|
||||
"""
|
||||
return obj.text[:50] + "..." if obj.text and len(obj.text) > 50 else obj.text
|
||||
|
||||
@admin.display(description="Комментарий (предпросмотр)", empty_value="—")
|
||||
def comment_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр комментария.
|
||||
"""
|
||||
return (
|
||||
obj.comment[:50] + "..."
|
||||
if obj.comment and len(obj.comment) > 50
|
||||
else obj.comment
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Ref1C)
|
||||
class Ref1CAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Ref1C.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"ref_key",
|
||||
"sync_enabled",
|
||||
"default_value",
|
||||
"last_sync_price",
|
||||
"last_updated",
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = ("ref_key",)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("sync_enabled",)
|
||||
|
||||
# Поля, которые можно редактировать прямо в списке
|
||||
list_editable = (
|
||||
"sync_enabled",
|
||||
"default_value",
|
||||
)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"ref_key",
|
||||
"sync_enabled",
|
||||
"default_value",
|
||||
"last_sync_price",
|
||||
)
|
||||
|
||||
# Добавляем поле "последнее обновление" (только для чтения)
|
||||
readonly_fields = ("last_updated",)
|
||||
|
||||
@admin.display(description="Последнее обновление")
|
||||
def last_updated(self, obj):
|
||||
"""
|
||||
Отображает дату последнего обновления записи.
|
||||
"""
|
||||
return obj.updated_at if hasattr(obj, "updated_at") else "Неизвестно"
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Переопределяем метод сохранения модели для добавления метки времени.
|
||||
"""
|
||||
# Если вы хотите отслеживать время обновления, добавьте поле updated_at в модель
|
||||
obj.save()
|
||||
|
||||
|
||||
@admin.register(FooterModel)
|
||||
class FooterModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели FooterModel.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"text_preview", # Кастомное поле для предпросмотра текста
|
||||
"small_text", # Флаг малого текста
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = ("text",)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("small_text",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"text",
|
||||
"small_text",
|
||||
)
|
||||
|
||||
@admin.display(description="Текст (предпросмотр)", empty_value="—")
|
||||
def text_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр текста нижнего колонтитула.
|
||||
"""
|
||||
return obj.text[:50] + "..." if obj.text and len(obj.text) > 50 else obj.text
|
||||
|
||||
|
||||
@admin.register(CustomRequest)
|
||||
class CustomRequestAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели CustomRequest.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"name", # Имя пользователя
|
||||
"phone", # Телефон пользователя
|
||||
"email", # Email пользователя
|
||||
"date", # Дата создания запроса
|
||||
"fence_info_preview", # Кастомное поле для предпросмотра информации о заборе
|
||||
"privacy", # Согласие на политику конфиденциальности
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"name",
|
||||
"phone",
|
||||
"email",
|
||||
"fence_info",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = (
|
||||
"privacy",
|
||||
"date",
|
||||
)
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ("date",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": ("name", "phone", "email", "date"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Информация о запросе",
|
||||
{
|
||||
"fields": ("fence_info", "privacy"),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Информация о заборе (предпросмотр)", empty_value="—")
|
||||
def fence_info_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр информации о заборе.
|
||||
"""
|
||||
return (
|
||||
obj.fence_info[:50] + "..."
|
||||
if obj.fence_info and len(obj.fence_info) > 50
|
||||
else obj.fence_info
|
||||
)
|
||||
|
||||
|
||||
@admin.register(SocialNetwork)
|
||||
class SocialNetworkAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели SocialNetwork.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"name", # Название социальной сети
|
||||
"link", # Ссылка на социальную сеть
|
||||
"icon_preview", # Кастомное поле для предпросмотра иконки
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"name",
|
||||
"link",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели (если нужно)
|
||||
list_filter = ()
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"name",
|
||||
"link",
|
||||
"icon",
|
||||
)
|
||||
|
||||
@admin.display(description="Иконка (предпросмотр)", empty_value="—")
|
||||
def icon_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр иконки.
|
||||
"""
|
||||
return obj.icon[:50] + "..." if obj.icon and len(obj.icon) > 50 else obj.icon
|
||||
|
||||
|
||||
@admin.register(LetterTemplate)
|
||||
class LetterTemplateAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели LetterTemplate.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"subject", # Тема письма
|
||||
"type", # Тип письма
|
||||
"copy_to", # Копия адресата
|
||||
"body_preview", # Кастомное поле для предпросмотра тела письма
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"subject",
|
||||
"body",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("type",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": ("subject", "type", "copy_to"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Тело письма",
|
||||
{
|
||||
"fields": ("body",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Текст письма (предпросмотр)", empty_value="—")
|
||||
def body_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр тела письма.
|
||||
"""
|
||||
return obj.body[:50] + "..." if obj.body and len(obj.body) > 50 else obj.body
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GlobalDataConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'global_data'
|
|
@ -0,0 +1,402 @@
|
|||
from slugify import slugify
|
||||
from django.db import models
|
||||
from django.core.validators import MaxValueValidator
|
||||
|
||||
from markdownfield.models import MarkdownField, RenderedMarkdownField
|
||||
from markdownfield.validators import VALIDATOR_STANDARD
|
||||
|
||||
from mns.utils import group_based_upload_to
|
||||
|
||||
class Seo(models.Model):
|
||||
"""
|
||||
Модель SEO содержит информацию о тексте, электронной почте, дополнительном тексте и изображении.
|
||||
"""
|
||||
|
||||
# Первичный ключ, автоинкрементируемый
|
||||
id = models.AutoField(primary_key=True, verbose_name="ID")
|
||||
|
||||
# Поле для хранения текста с максимальной длиной 200 символов
|
||||
text = models.CharField(
|
||||
max_length=200, help_text="Введите текст", verbose_name="Текст"
|
||||
)
|
||||
|
||||
# Поле для хранения email-адреса
|
||||
email = models.EmailField(help_text="Введите Email", verbose_name="Email")
|
||||
|
||||
# Поле для хранения дополнительного текста, может быть пустым
|
||||
blank = models.TextField(blank=True, null=True, verbose_name="Дополнительный текст")
|
||||
|
||||
# Поле для хранения изображения, загружается по указанному пути
|
||||
image = models.ImageField(
|
||||
upload_to=group_based_upload_to,
|
||||
default="", # Пустая строка в качестве значения по умолчанию
|
||||
verbose_name="Изображение",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.text}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "SEO запись"
|
||||
verbose_name_plural = "SEO записи"
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
"""
|
||||
Модель Page содержит информацию о странице, включая порядок отображения,
|
||||
заголовки меню и страницы, содержимое, слаг, изображение и внешнюю ссылку.
|
||||
"""
|
||||
|
||||
# Порядок отображения страницы
|
||||
order = models.IntegerField(
|
||||
verbose_name="Порядок",
|
||||
help_text="Введите порядковый номер страницы для сортировки",
|
||||
)
|
||||
|
||||
# Заголовок меню
|
||||
menu_title = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Заголовок меню",
|
||||
help_text="Введите заголовок, который будет отображаться в меню",
|
||||
)
|
||||
|
||||
# Заголовок страницы
|
||||
title = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Заголовок страницы",
|
||||
help_text="Введите заголовок страницы",
|
||||
)
|
||||
|
||||
# Содержимое страницы в формате Markdown
|
||||
content = MarkdownField(
|
||||
rendered_field="content_rendered",
|
||||
validator=VALIDATOR_STANDARD,
|
||||
verbose_name="Содержимое (Markdown)",
|
||||
help_text="Введите содержимое страницы в формате Markdown",
|
||||
)
|
||||
|
||||
# Отрендеренное содержимое страницы
|
||||
content_rendered = RenderedMarkdownField(
|
||||
verbose_name="Отрендеренное содержимое",
|
||||
help_text="Это поле автоматически генерируется из поля 'Содержимое'",
|
||||
)
|
||||
|
||||
# Slug для URL страницы
|
||||
slug = models.SlugField(
|
||||
unique=True,
|
||||
verbose_name="Slug",
|
||||
help_text="Введите уникальный slug для URL страницы",
|
||||
)
|
||||
|
||||
# Изображение страницы
|
||||
image = models.ImageField(
|
||||
upload_to=group_based_upload_to,
|
||||
default=None, # Убедитесь, что это значение корректно
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Изображение",
|
||||
help_text="Выберите изображение для страницы",
|
||||
)
|
||||
|
||||
# Внешняя ссылка
|
||||
external_link = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Внешняя ссылка",
|
||||
help_text="Введите внешнюю ссылку, если она есть",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.menu_title} #{self.slug} {self.order}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Страница"
|
||||
verbose_name_plural = "Страницы"
|
||||
|
||||
|
||||
class Menu(models.Model):
|
||||
"""
|
||||
Модель Menu содержит информацию о меню, включая страницы и тип меню.
|
||||
"""
|
||||
|
||||
# Связь с моделью Page (многие ко многим)
|
||||
pages = models.ManyToManyField(
|
||||
Page,
|
||||
verbose_name="Страницы",
|
||||
help_text="Выберите страницы, которые будут включены в меню",
|
||||
)
|
||||
|
||||
# Тип меню (выбор из списка)
|
||||
type = models.IntegerField(
|
||||
choices=[
|
||||
(1, "TopMenu"),
|
||||
],
|
||||
unique=True,
|
||||
verbose_name="Тип меню",
|
||||
help_text="Выберите тип меню",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
menu_type = dict(self._meta.get_field("type").choices).get(
|
||||
self.type, "Неизвестно"
|
||||
)
|
||||
return f"Меню {menu_type}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Меню"
|
||||
verbose_name_plural = "Меню"
|
||||
|
||||
|
||||
class ReviewModel(models.Model):
|
||||
"""
|
||||
Модель ReviewModel содержит информацию о отзывах, включая изображение, текст и комментарий.
|
||||
"""
|
||||
|
||||
# Изображение отзыва
|
||||
image = models.ImageField(
|
||||
upload_to=group_based_upload_to,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Изображение",
|
||||
help_text="Выберите изображение для отзыва",
|
||||
)
|
||||
|
||||
# Текст отзыва
|
||||
text = models.CharField(
|
||||
max_length=200, # Установите максимальную длину строки
|
||||
verbose_name="Текст",
|
||||
help_text="Введите текст отзыва",
|
||||
)
|
||||
|
||||
# Комментарий к отзыву
|
||||
comment = models.TextField(
|
||||
verbose_name="Комментарий", help_text="Введите комментарий к отзыву"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.text}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Отзыв"
|
||||
verbose_name_plural = "Отзывы"
|
||||
|
||||
|
||||
class Ref1C(models.Model):
|
||||
"""
|
||||
Модель для хранения ссылки на данные из 1С (Ref_Key), флага для включения синхронизации,
|
||||
последней синхронизированной цены и значения по умолчанию.
|
||||
"""
|
||||
|
||||
ref_key = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name="Ref_Key",
|
||||
help_text="Уникальный идентификатор из 1С",
|
||||
)
|
||||
sync_enabled = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Включить синхронизацию",
|
||||
help_text="Флаг для включения синхронизации данных из 1С",
|
||||
)
|
||||
default_value = models.DecimalField(
|
||||
max_digits=7,
|
||||
decimal_places=2,
|
||||
verbose_name="Значение по умолчанию",
|
||||
help_text="Значение, используемое по умолчанию или при отсутствии данных из 1С",
|
||||
)
|
||||
last_sync_price = models.DecimalField(
|
||||
max_digits=7,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Последняя синхронизированная цена",
|
||||
help_text="Последняя цена, полученная из 1С",
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name="Последнее обновление",
|
||||
help_text="Дата и время последнего обновления записи"
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Ref_Key: {self.ref_key}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Ссылка на 1С"
|
||||
verbose_name_plural = "Ссылки на 1С"
|
||||
|
||||
|
||||
class FooterModel(models.Model):
|
||||
"""
|
||||
Модель FooterModel содержит информацию о нижнем колонтитуле, включая текст и флаг малого текста.
|
||||
"""
|
||||
|
||||
# Текст нижнего колонтитула
|
||||
text = models.TextField(
|
||||
max_length=1000,
|
||||
verbose_name="Текст",
|
||||
help_text="Введите текст нижнего колонтитула",
|
||||
)
|
||||
|
||||
# Флаг малого текста
|
||||
small_text = models.BooleanField(
|
||||
verbose_name="Малый текст",
|
||||
help_text="Отметьте, если текст должен быть маленьким",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.text}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Нижний колонтитул"
|
||||
verbose_name_plural = "Нижние колонтитулы"
|
||||
|
||||
|
||||
class CustomRequest(models.Model):
|
||||
"""
|
||||
Модель CustomRequest содержит информацию о пользовательских запросах,
|
||||
включая имя, телефон, email, дату создания, информацию о заборе и согласие на политику конфиденциальности.
|
||||
"""
|
||||
|
||||
# Имя пользователя
|
||||
name = models.CharField(
|
||||
max_length=100, verbose_name="Имя", help_text="Введите имя пользователя"
|
||||
)
|
||||
|
||||
# Телефон пользователя (необязательное поле)
|
||||
phone = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Телефон",
|
||||
help_text="Введите телефон пользователя",
|
||||
)
|
||||
|
||||
# Email пользователя (необязательное поле)
|
||||
email = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Email",
|
||||
help_text="Введите email пользователя",
|
||||
)
|
||||
|
||||
# Дата и время создания запроса
|
||||
date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name="Дата создания",
|
||||
help_text="Дата и время создания запроса",
|
||||
)
|
||||
|
||||
# Информация о заборе
|
||||
fence_info = models.TextField(
|
||||
verbose_name="Информация о заборе", help_text="Введите информацию о заборе"
|
||||
)
|
||||
|
||||
# Согласие на политику конфиденциальности
|
||||
privacy = models.BooleanField(
|
||||
verbose_name="Политика конфиденциальности",
|
||||
help_text="Отметьте, если пользователь согласен с политикой конфиденциальности",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.name} {self.phone} {self.date}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Пользовательский запрос"
|
||||
verbose_name_plural = "Пользовательские запросы"
|
||||
|
||||
|
||||
class SocialNetwork(models.Model):
|
||||
"""
|
||||
Модель SocialNetwork содержит информацию о социальных сетях, включая название, ссылку и иконку.
|
||||
"""
|
||||
|
||||
# Название социальной сети
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name="Название",
|
||||
help_text="Введите название социальной сети",
|
||||
)
|
||||
|
||||
# Ссылка на социальную сеть
|
||||
link = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name="Ссылка",
|
||||
help_text="Введите ссылку на социальную сеть",
|
||||
)
|
||||
|
||||
# Иконка социальной сети (необязательное поле)
|
||||
icon = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Иконка",
|
||||
help_text="Введите HTML-код или путь к иконке социальной сети",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Социальная сеть"
|
||||
verbose_name_plural = "Социальные сети"
|
||||
|
||||
|
||||
class LetterTemplate(models.Model):
|
||||
"""
|
||||
Модель LetterTemplate содержит информацию о шаблонах писем, включая тему, тело письма, копию адресата и тип письма.
|
||||
"""
|
||||
|
||||
# Тема письма
|
||||
subject = models.CharField(
|
||||
max_length=100, verbose_name="Тема письма", help_text="Введите тему письма"
|
||||
)
|
||||
|
||||
# Тело письма
|
||||
body = models.TextField(
|
||||
verbose_name="Текст письма", help_text="Введите текст письма"
|
||||
)
|
||||
|
||||
# Копия адресата (необязательное поле)
|
||||
copy_to = models.EmailField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Копия адресата",
|
||||
help_text="Введите email для копии письма",
|
||||
)
|
||||
|
||||
# Тип письма (выбор из списка)
|
||||
type = models.CharField(
|
||||
choices=[
|
||||
("user_submit", "Submit"),
|
||||
],
|
||||
unique=True,
|
||||
verbose_name="Тип письма",
|
||||
help_text="Выберите тип письма",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.subject}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Шаблон письма"
|
||||
verbose_name_plural = "Шаблоны писем"
|
|
@ -0,0 +1,68 @@
|
|||
from rest_framework import serializers
|
||||
from mns.logger import logger
|
||||
|
||||
from .models import (
|
||||
CustomRequest,
|
||||
FooterModel,
|
||||
LetterTemplate,
|
||||
Page,
|
||||
ReviewModel,
|
||||
Seo,
|
||||
SocialNetwork,
|
||||
Menu,
|
||||
)
|
||||
|
||||
|
||||
class SeoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Seo
|
||||
fields = ["id", "text", "email", "blank", "image"]
|
||||
|
||||
|
||||
class PageSerializer(serializers.ModelSerializer):
|
||||
image = serializers.ImageField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class MenuSerializer(serializers.ModelSerializer):
|
||||
pages = PageSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = "__all__"
|
||||
depth = 2
|
||||
|
||||
|
||||
class ReviewSerializer(serializers.ModelSerializer):
|
||||
image = serializers.ImageField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = ReviewModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class FooterSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FooterModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CustomRequestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomRequest
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SocialNetworkSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SocialNetwork
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class LetterTemplateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = LetterTemplate
|
||||
fields = "__all__"
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,144 @@
|
|||
import re
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import viewsets, filters
|
||||
|
||||
from mns.logger import logger
|
||||
|
||||
from .models import (
|
||||
CustomRequest,
|
||||
FooterModel,
|
||||
LetterTemplate,
|
||||
Page,
|
||||
ReviewModel,
|
||||
Seo,
|
||||
SocialNetwork,
|
||||
Menu,
|
||||
)
|
||||
from .serializers import (
|
||||
CustomRequestSerializer,
|
||||
FooterSerializer,
|
||||
LetterTemplateSerializer,
|
||||
PageSerializer,
|
||||
ReviewSerializer,
|
||||
SeoSerializer,
|
||||
SocialNetworkSerializer,
|
||||
MenuSerializer,
|
||||
)
|
||||
|
||||
|
||||
# Create your views here.
|
||||
class SeoViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows partners to be viewed or edited.
|
||||
"""
|
||||
|
||||
http_method_names = ["get"]
|
||||
queryset = Seo.objects.all()
|
||||
serializer_class = SeoSerializer
|
||||
|
||||
class PageViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = Page.objects.all()
|
||||
serializer_class = PageSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
lookup_field = 'slug'
|
||||
|
||||
|
||||
class MenuViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = Menu.objects.all()
|
||||
serializer_class = MenuSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
|
||||
|
||||
class ReviewViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = ReviewModel.objects.all()
|
||||
serializer_class = ReviewSerializer
|
||||
|
||||
|
||||
class FooterViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = FooterModel.objects.all()
|
||||
serializer_class = FooterSerializer
|
||||
|
||||
|
||||
class CustomRequestViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["options", "post"]
|
||||
authentication_classes = ()
|
||||
queryset = CustomRequest.objects.all()
|
||||
serializer_class = CustomRequestSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
name = self.request.data.get("name")
|
||||
phone = self.request.data.get("phone")
|
||||
email = self.request.data.get("email")
|
||||
fence_info = self.request.data.get("fence_info")
|
||||
total = re.findall(r"Итого.*?(\d+\s?\d+,?\d+).*?₽", fence_info)
|
||||
total_price = 0 if not len(total) else re.sub(r"\s", "", "" or total[0])
|
||||
data = {
|
||||
"fields[TITLE]": f"КУПИЗАБОР {name} {email} {phone}",
|
||||
"fields[NAME]": name,
|
||||
"fields[STATUS_ID]": "NEW",
|
||||
"fields[OPENED]": "Y",
|
||||
# "fields[ASSIGNED_BY_ID]": "34",
|
||||
"fields[PHONE][0][VALUE]": phone,
|
||||
"fields[PHONE][0][VALUE_TYPE]": "WORK",
|
||||
"fields[EMAIL][0][VALUE]": email,
|
||||
"fields[EMAIL][0][VALUE_TYPE]": "WORK",
|
||||
"fields[SOURCE_ID]": "1",
|
||||
"fields[OPPORTUNITY]": total_price,
|
||||
"fields[UF_CRM_1713510698]": fence_info,
|
||||
"fields[UF_CRM_1712154805]": 47,
|
||||
"fields[UF_CRM_1712154416]": datetime.now() + timedelta(days=1),
|
||||
}
|
||||
bx_result = requests.post(
|
||||
f"{settings.BX_WEBHOOK}/crm.lead.add.json",
|
||||
data,
|
||||
)
|
||||
bx_result_json = bx_result.json()
|
||||
if bx_result_json["result"]:
|
||||
logger.info(f'Был создан лид {bx_result_json["result"]}')
|
||||
letter_template = LetterTemplate.objects.get(type="user_submit")
|
||||
if email:
|
||||
todo_data = {
|
||||
"fields[OWNER_TYPE_ID]": 1,
|
||||
"fields[OWNER_ID]": bx_result_json["result"],
|
||||
"fields[TYPE_ID]": 4,
|
||||
"fields[SUBJECT]": (
|
||||
"КУПИЗАБОР" if not letter_template else letter_template.subject
|
||||
),
|
||||
"fields[DESCRIPTION]": (
|
||||
f"Данные расчета\n{fence_info}"
|
||||
if not letter_template
|
||||
else re.sub("\[fence\]", fence_info, letter_template.body)
|
||||
),
|
||||
"fields[DESCRIPTION_TYPE]": 2,
|
||||
"fields[COMMUNICATIONS][0][VALUE]": email,
|
||||
"fields[COMPLETED]": "Y",
|
||||
"fields[DIRECTION]": 2,
|
||||
"fields[SETTINGS][MESSAGE_FROM]": "СВС технологии <office@svs-tech.pro>",
|
||||
}
|
||||
bx_result_email = requests.post(
|
||||
f"{settings.BX_WEBHOOK}/crm.activity.add",
|
||||
todo_data,
|
||||
)
|
||||
email_result = bx_result_email.json()
|
||||
if email_result:
|
||||
logger.info(f"Было отправлено письмо {email_result['result']}")
|
||||
|
||||
|
||||
class SocialNetworkViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = SocialNetwork.objects.all()
|
||||
serializer_class = SocialNetworkSerializer
|
||||
|
||||
|
||||
class LetterTemplatesViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = LetterTemplate.objects.all()
|
||||
serializer_class = LetterTemplateSerializer
|
|
@ -1,61 +1,10 @@
|
|||
from django.contrib import admin
|
||||
from .models import (
|
||||
AdvantageModel,
|
||||
CalcModel,
|
||||
CustomRequest,
|
||||
DiscountModel,
|
||||
FooterModel,
|
||||
KupiZabor,
|
||||
LetterTemplate,
|
||||
Page,
|
||||
ReviewModel,
|
||||
Seo,
|
||||
SocialNetwork,
|
||||
Menu,
|
||||
Ref1C,
|
||||
)
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(Seo)
|
||||
class SeoAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Seo.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"id",
|
||||
"text",
|
||||
"email",
|
||||
"has_image", # Кастомное поле для проверки наличия изображения
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"text",
|
||||
"email",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("email",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"text",
|
||||
"email",
|
||||
"blank",
|
||||
"image",
|
||||
)
|
||||
|
||||
@admin.display(description="Есть изображение?", boolean=True)
|
||||
def has_image(self, obj):
|
||||
"""
|
||||
Проверяет, загружено ли изображение для записи.
|
||||
"""
|
||||
return bool(obj.image)
|
||||
|
||||
|
||||
@admin.register(KupiZabor)
|
||||
class KupiZaborAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
|
@ -113,156 +62,6 @@ class KupiZaborAdmin(admin.ModelAdmin):
|
|||
)
|
||||
|
||||
|
||||
@admin.register(Page)
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели PageModel.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"order",
|
||||
"menu_title",
|
||||
"title",
|
||||
"slug_preview", # Кастомное поле для предпросмотра slug
|
||||
"has_image", # Кастомное поле для проверки наличия изображения
|
||||
"external_link", # Внешняя ссылка
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"menu_title",
|
||||
"title",
|
||||
"slug",
|
||||
"content",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("order",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": ("order", "menu_title", "title", "slug"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Содержимое",
|
||||
{
|
||||
"fields": ("content", "content_rendered"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Медиа и ссылки",
|
||||
{
|
||||
"fields": ("image", "external_link"),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ("content_rendered",)
|
||||
|
||||
@admin.display(description="Slug (предпросмотр)", empty_value="—")
|
||||
def slug_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр slug.
|
||||
"""
|
||||
return obj.slug[:50] + "..." if obj.slug and len(obj.slug) > 50 else obj.slug
|
||||
|
||||
@admin.display(description="Есть изображение?", boolean=True)
|
||||
def has_image(self, obj):
|
||||
"""
|
||||
Проверяет, загружено ли изображение для записи.
|
||||
"""
|
||||
return bool(obj.image)
|
||||
|
||||
|
||||
@admin.register(Menu)
|
||||
class MenuAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Menu.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"type_display", # Кастомное поле для отображения типа меню
|
||||
"pages_count", # Кастомное поле для подсчета страниц в меню
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("type",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
filter_horizontal = ("pages",) # Удобный виджет для выбора страниц
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ()
|
||||
|
||||
@admin.display(description="Тип меню")
|
||||
def type_display(self, obj):
|
||||
"""
|
||||
Отображает читаемое название типа меню.
|
||||
"""
|
||||
return dict(obj._meta.get_field("type").choices).get(obj.type, "Неизвестно")
|
||||
|
||||
@admin.display(description="Количество страниц")
|
||||
def pages_count(self, obj):
|
||||
"""
|
||||
Подсчитывает количество страниц, связанных с меню.
|
||||
"""
|
||||
return obj.pages.count()
|
||||
|
||||
|
||||
@admin.register(ReviewModel)
|
||||
class ReviewModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели ReviewModel.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"text_preview", # Кастомное поле для предпросмотра текста отзыва
|
||||
"comment_preview", # Кастомное поле для предпросмотра комментария
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"text",
|
||||
"comment",
|
||||
)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"image",
|
||||
"text",
|
||||
"comment",
|
||||
)
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ()
|
||||
|
||||
@admin.display(description="Текст (предпросмотр)", empty_value="—")
|
||||
def text_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр текста отзыва.
|
||||
"""
|
||||
return obj.text[:50] + "..." if obj.text and len(obj.text) > 50 else obj.text
|
||||
|
||||
@admin.display(description="Комментарий (предпросмотр)", empty_value="—")
|
||||
def comment_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр комментария.
|
||||
"""
|
||||
return (
|
||||
obj.comment[:50] + "..."
|
||||
if obj.comment and len(obj.comment) > 50
|
||||
else obj.comment
|
||||
)
|
||||
|
||||
|
||||
@admin.register(DiscountModel)
|
||||
class DiscountModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
|
@ -303,59 +102,6 @@ class DiscountModelAdmin(admin.ModelAdmin):
|
|||
return f"От {obj.min_quantity}"
|
||||
|
||||
|
||||
@admin.register(Ref1C)
|
||||
class Ref1CAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели Ref1C.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"ref_key",
|
||||
"sync_enabled",
|
||||
"default_value",
|
||||
"last_sync_price",
|
||||
"last_updated",
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = ("ref_key",)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("sync_enabled",)
|
||||
|
||||
# Поля, которые можно редактировать прямо в списке
|
||||
list_editable = (
|
||||
"sync_enabled",
|
||||
"default_value",
|
||||
)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"ref_key",
|
||||
"sync_enabled",
|
||||
"default_value",
|
||||
"last_sync_price",
|
||||
)
|
||||
|
||||
# Добавляем поле "последнее обновление" (только для чтения)
|
||||
readonly_fields = ("last_updated",)
|
||||
|
||||
@admin.display(description="Последнее обновление")
|
||||
def last_updated(self, obj):
|
||||
"""
|
||||
Отображает дату последнего обновления записи.
|
||||
"""
|
||||
return obj.updated_at if hasattr(obj, "updated_at") else "Неизвестно"
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Переопределяем метод сохранения модели для добавления метки времени.
|
||||
"""
|
||||
# Если вы хотите отслеживать время обновления, добавьте поле updated_at в модель
|
||||
obj.save()
|
||||
|
||||
|
||||
@admin.register(CalcModel)
|
||||
class CalcModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
|
@ -440,219 +186,3 @@ class CalcModelAdmin(admin.ModelAdmin):
|
|||
", ".join([str(discount) for discount in obj.discount.all()])
|
||||
or "Нет скидок"
|
||||
)
|
||||
|
||||
|
||||
@admin.register(AdvantageModel)
|
||||
class AdvantageModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели AdvantageModel.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"title", # Заголовок
|
||||
"content_preview", # Кастомное поле для предпросмотра содержимого
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"title",
|
||||
"content",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели (если нужно)
|
||||
list_filter = ()
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"title",
|
||||
"content",
|
||||
)
|
||||
|
||||
@admin.display(description="Содержимое (предпросмотр)", empty_value="—")
|
||||
def content_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр содержимого.
|
||||
"""
|
||||
return (
|
||||
obj.content[:50] + "..."
|
||||
if obj.content and len(obj.content) > 50
|
||||
else obj.content
|
||||
)
|
||||
|
||||
|
||||
@admin.register(FooterModel)
|
||||
class FooterModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели FooterModel.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"text_preview", # Кастомное поле для предпросмотра текста
|
||||
"small_text", # Флаг малого текста
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = ("text",)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("small_text",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"text",
|
||||
"small_text",
|
||||
)
|
||||
|
||||
@admin.display(description="Текст (предпросмотр)", empty_value="—")
|
||||
def text_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр текста нижнего колонтитула.
|
||||
"""
|
||||
return obj.text[:50] + "..." if obj.text and len(obj.text) > 50 else obj.text
|
||||
|
||||
|
||||
@admin.register(CustomRequest)
|
||||
class CustomRequestAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели CustomRequest.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"name", # Имя пользователя
|
||||
"phone", # Телефон пользователя
|
||||
"email", # Email пользователя
|
||||
"date", # Дата создания запроса
|
||||
"fence_info_preview", # Кастомное поле для предпросмотра информации о заборе
|
||||
"privacy", # Согласие на политику конфиденциальности
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"name",
|
||||
"phone",
|
||||
"email",
|
||||
"fence_info",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = (
|
||||
"privacy",
|
||||
"date",
|
||||
)
|
||||
|
||||
# Поля, доступные только для чтения
|
||||
readonly_fields = ("date",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": ("name", "phone", "email", "date"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Информация о запросе",
|
||||
{
|
||||
"fields": ("fence_info", "privacy"),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Информация о заборе (предпросмотр)", empty_value="—")
|
||||
def fence_info_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр информации о заборе.
|
||||
"""
|
||||
return (
|
||||
obj.fence_info[:50] + "..."
|
||||
if obj.fence_info and len(obj.fence_info) > 50
|
||||
else obj.fence_info
|
||||
)
|
||||
|
||||
|
||||
@admin.register(SocialNetwork)
|
||||
class SocialNetworkAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели SocialNetwork.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"name", # Название социальной сети
|
||||
"link", # Ссылка на социальную сеть
|
||||
"icon_preview", # Кастомное поле для предпросмотра иконки
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"name",
|
||||
"link",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели (если нужно)
|
||||
list_filter = ()
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fields = (
|
||||
"name",
|
||||
"link",
|
||||
"icon",
|
||||
)
|
||||
|
||||
@admin.display(description="Иконка (предпросмотр)", empty_value="—")
|
||||
def icon_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр иконки.
|
||||
"""
|
||||
return obj.icon[:50] + "..." if obj.icon and len(obj.icon) > 50 else obj.icon
|
||||
|
||||
|
||||
@admin.register(LetterTemplate)
|
||||
class LetterTemplateAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Класс админки для модели LetterTemplate.
|
||||
"""
|
||||
|
||||
# Поля, которые будут отображаться в списке записей
|
||||
list_display = (
|
||||
"subject", # Тема письма
|
||||
"type", # Тип письма
|
||||
"copy_to", # Копия адресата
|
||||
"body_preview", # Кастомное поле для предпросмотра тела письма
|
||||
)
|
||||
|
||||
# Поля, по которым можно выполнять поиск
|
||||
search_fields = (
|
||||
"subject",
|
||||
"body",
|
||||
)
|
||||
|
||||
# Фильтры для боковой панели
|
||||
list_filter = ("type",)
|
||||
|
||||
# Поля, которые будут показаны в форме редактирования
|
||||
fieldsets = (
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": ("subject", "type", "copy_to"),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Тело письма",
|
||||
{
|
||||
"fields": ("body",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Текст письма (предпросмотр)", empty_value="—")
|
||||
def body_preview(self, obj):
|
||||
"""
|
||||
Отображает краткий предпросмотр тела письма.
|
||||
"""
|
||||
return obj.body[:50] + "..." if obj.body and len(obj.body) > 50 else obj.body
|
||||
|
|
|
@ -2,55 +2,10 @@ from slugify import slugify
|
|||
from django.db import models
|
||||
from django.core.validators import MaxValueValidator
|
||||
|
||||
from markdownfield.models import MarkdownField, RenderedMarkdownField
|
||||
from markdownfield.validators import VALIDATOR_STANDARD
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("root")
|
||||
|
||||
|
||||
def group_based_upload_to(instance, filename):
|
||||
logger.info(instance)
|
||||
return "files/image/{}/{}/{}".format(
|
||||
type(instance).__name__.lower(), instance.id, filename
|
||||
)
|
||||
|
||||
|
||||
class Seo(models.Model):
|
||||
"""
|
||||
Модель SEO содержит информацию о тексте, электронной почте, дополнительном тексте и изображении.
|
||||
"""
|
||||
|
||||
# Первичный ключ, автоинкрементируемый
|
||||
id = models.AutoField(primary_key=True, verbose_name="ID")
|
||||
|
||||
# Поле для хранения текста с максимальной длиной 200 символов
|
||||
text = models.CharField(
|
||||
max_length=200, help_text="Введите текст", verbose_name="Текст"
|
||||
)
|
||||
|
||||
# Поле для хранения email-адреса
|
||||
email = models.EmailField(help_text="Введите Email", verbose_name="Email")
|
||||
|
||||
# Поле для хранения дополнительного текста, может быть пустым
|
||||
blank = models.TextField(blank=True, null=True, verbose_name="Дополнительный текст")
|
||||
|
||||
# Поле для хранения изображения, загружается по указанному пути
|
||||
image = models.ImageField(
|
||||
upload_to=group_based_upload_to,
|
||||
default="", # Пустая строка в качестве значения по умолчанию
|
||||
verbose_name="Изображение",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.text}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "SEO запись"
|
||||
verbose_name_plural = "SEO записи"
|
||||
|
||||
from global_data.models import Ref1C
|
||||
|
||||
from mns.logger import logger
|
||||
from mns.utils import group_based_upload_to
|
||||
|
||||
class KupiZabor(models.Model):
|
||||
"""
|
||||
|
@ -114,151 +69,34 @@ class KupiZabor(models.Model):
|
|||
verbose_name_plural = "Страницы 'KupiZabor'"
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
class AdvantageModel(models.Model):
|
||||
"""
|
||||
Модель PageModel содержит информацию о странице, включая порядок отображения,
|
||||
заголовки меню и страницы, содержимое, слаг, изображение и внешнюю ссылку.
|
||||
Модель AdvantageModel содержит информацию о преимуществах, включая заголовок и содержимое.
|
||||
"""
|
||||
|
||||
# Порядок отображения страницы
|
||||
order = models.IntegerField(
|
||||
verbose_name="Порядок",
|
||||
help_text="Введите порядковый номер страницы для сортировки",
|
||||
)
|
||||
|
||||
# Заголовок меню
|
||||
menu_title = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Заголовок меню",
|
||||
help_text="Введите заголовок, который будет отображаться в меню",
|
||||
)
|
||||
|
||||
# Заголовок страницы
|
||||
# Заголовок
|
||||
title = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name="Заголовок страницы",
|
||||
help_text="Введите заголовок страницы",
|
||||
)
|
||||
|
||||
# Содержимое страницы в формате Markdown
|
||||
content = MarkdownField(
|
||||
rendered_field="content_rendered",
|
||||
validator=VALIDATOR_STANDARD,
|
||||
verbose_name="Содержимое (Markdown)",
|
||||
help_text="Введите содержимое страницы в формате Markdown",
|
||||
)
|
||||
|
||||
# Отрендеренное содержимое страницы
|
||||
content_rendered = RenderedMarkdownField(
|
||||
verbose_name="Отрендеренное содержимое",
|
||||
help_text="Это поле автоматически генерируется из поля 'Содержимое'",
|
||||
)
|
||||
|
||||
# Slug для URL страницы
|
||||
slug = models.SlugField(
|
||||
unique=True,
|
||||
verbose_name="Slug",
|
||||
help_text="Введите уникальный slug для URL страницы",
|
||||
)
|
||||
|
||||
# Изображение страницы
|
||||
image = models.ImageField(
|
||||
upload_to=group_based_upload_to,
|
||||
default=None, # Убедитесь, что это значение корректно
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Изображение",
|
||||
help_text="Выберите изображение для страницы",
|
||||
)
|
||||
|
||||
# Внешняя ссылка
|
||||
external_link = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Внешняя ссылка",
|
||||
help_text="Введите внешнюю ссылку, если она есть",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.menu_title} #{self.slug} {self.order}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Страница"
|
||||
verbose_name_plural = "Страницы"
|
||||
|
||||
|
||||
class Menu(models.Model):
|
||||
"""
|
||||
Модель Menu содержит информацию о меню, включая страницы и тип меню.
|
||||
"""
|
||||
|
||||
# Связь с моделью PageModel (многие ко многим)
|
||||
pages = models.ManyToManyField(
|
||||
PageModel,
|
||||
verbose_name="Страницы",
|
||||
help_text="Выберите страницы, которые будут включены в меню",
|
||||
)
|
||||
|
||||
# Тип меню (выбор из списка)
|
||||
type = models.IntegerField(
|
||||
choices=[
|
||||
(1, "TopMenu"),
|
||||
],
|
||||
unique=True,
|
||||
verbose_name="Тип меню",
|
||||
help_text="Выберите тип меню",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
menu_type = dict(self._meta.get_field("type").choices).get(
|
||||
self.type, "Неизвестно"
|
||||
)
|
||||
return f"Меню {menu_type}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Меню"
|
||||
verbose_name_plural = "Меню"
|
||||
|
||||
|
||||
class ReviewModel(models.Model):
|
||||
"""
|
||||
Модель ReviewModel содержит информацию о отзывах, включая изображение, текст и комментарий.
|
||||
"""
|
||||
|
||||
# Изображение отзыва
|
||||
image = models.ImageField(
|
||||
upload_to=group_based_upload_to,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Изображение",
|
||||
help_text="Выберите изображение для отзыва",
|
||||
)
|
||||
|
||||
# Текст отзыва
|
||||
text = models.CharField(
|
||||
max_length=200, # Установите максимальную длину строки
|
||||
verbose_name="Текст",
|
||||
help_text="Введите текст отзыва",
|
||||
verbose_name="Заголовок",
|
||||
help_text="Введите заголовок",
|
||||
)
|
||||
|
||||
# Комментарий к отзыву
|
||||
comment = models.TextField(
|
||||
verbose_name="Комментарий", help_text="Введите комментарий к отзыву"
|
||||
# Содержимое
|
||||
content = models.CharField(
|
||||
max_length=200, # Установите максимальную длину строки
|
||||
verbose_name="Содержимое",
|
||||
help_text="Введите содержимое",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.text}"
|
||||
return f"{self.title} {self.content}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Отзыв"
|
||||
verbose_name_plural = "Отзывы"
|
||||
verbose_name = "Преимущество"
|
||||
verbose_name_plural = "Преимущества"
|
||||
|
||||
|
||||
class DiscountModel(models.Model):
|
||||
|
@ -302,51 +140,6 @@ class DiscountModel(models.Model):
|
|||
verbose_name_plural = "Скидки"
|
||||
|
||||
|
||||
class Ref1C(models.Model):
|
||||
"""
|
||||
Модель для хранения ссылки на данные из 1С (Ref_Key), флага для включения синхронизации,
|
||||
последней синхронизированной цены и значения по умолчанию.
|
||||
"""
|
||||
|
||||
ref_key = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name="Ref_Key",
|
||||
help_text="Уникальный идентификатор из 1С",
|
||||
)
|
||||
sync_enabled = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Включить синхронизацию",
|
||||
help_text="Флаг для включения синхронизации данных из 1С",
|
||||
)
|
||||
default_value = models.DecimalField(
|
||||
max_digits=7,
|
||||
decimal_places=2,
|
||||
verbose_name="Значение по умолчанию",
|
||||
help_text="Значение, используемое по умолчанию или при отсутствии данных из 1С",
|
||||
)
|
||||
last_sync_price = models.DecimalField(
|
||||
max_digits=7,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Последняя синхронизированная цена",
|
||||
help_text="Последняя цена, полученная из 1С",
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name="Последнее обновление",
|
||||
help_text="Дата и время последнего обновления записи"
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Ref_Key: {self.ref_key}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Ссылка на 1С"
|
||||
verbose_name_plural = "Ссылки на 1С"
|
||||
|
||||
|
||||
class CalcModel(models.Model):
|
||||
"""
|
||||
Модель CalcModel содержит информацию о калькуляторе стоимости, включая заголовок,
|
||||
|
@ -462,202 +255,3 @@ class CalcModel(models.Model):
|
|||
class Meta:
|
||||
verbose_name = "Калькулятор стоимости"
|
||||
verbose_name_plural = "Калькуляторы стоимости"
|
||||
|
||||
|
||||
class AdvantageModel(models.Model):
|
||||
"""
|
||||
Модель AdvantageModel содержит информацию о преимуществах, включая заголовок и содержимое.
|
||||
"""
|
||||
|
||||
# Заголовок
|
||||
title = models.CharField(
|
||||
max_length=200, # Установите максимальную длину строки
|
||||
verbose_name="Заголовок",
|
||||
help_text="Введите заголовок",
|
||||
)
|
||||
|
||||
# Содержимое
|
||||
content = models.CharField(
|
||||
max_length=200, # Установите максимальную длину строки
|
||||
verbose_name="Содержимое",
|
||||
help_text="Введите содержимое",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.title} {self.content}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Преимущество"
|
||||
verbose_name_plural = "Преимущества"
|
||||
|
||||
|
||||
class FooterModel(models.Model):
|
||||
"""
|
||||
Модель FooterModel содержит информацию о нижнем колонтитуле, включая текст и флаг малого текста.
|
||||
"""
|
||||
|
||||
# Текст нижнего колонтитула
|
||||
text = models.TextField(
|
||||
max_length=1000,
|
||||
verbose_name="Текст",
|
||||
help_text="Введите текст нижнего колонтитула",
|
||||
)
|
||||
|
||||
# Флаг малого текста
|
||||
small_text = models.BooleanField(
|
||||
verbose_name="Малый текст",
|
||||
help_text="Отметьте, если текст должен быть маленьким",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.text}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Нижний колонтитул"
|
||||
verbose_name_plural = "Нижние колонтитулы"
|
||||
|
||||
|
||||
class CustomRequest(models.Model):
|
||||
"""
|
||||
Модель CustomRequest содержит информацию о пользовательских запросах,
|
||||
включая имя, телефон, email, дату создания, информацию о заборе и согласие на политику конфиденциальности.
|
||||
"""
|
||||
|
||||
# Имя пользователя
|
||||
name = models.CharField(
|
||||
max_length=100, verbose_name="Имя", help_text="Введите имя пользователя"
|
||||
)
|
||||
|
||||
# Телефон пользователя (необязательное поле)
|
||||
phone = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Телефон",
|
||||
help_text="Введите телефон пользователя",
|
||||
)
|
||||
|
||||
# Email пользователя (необязательное поле)
|
||||
email = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Email",
|
||||
help_text="Введите email пользователя",
|
||||
)
|
||||
|
||||
# Дата и время создания запроса
|
||||
date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name="Дата создания",
|
||||
help_text="Дата и время создания запроса",
|
||||
)
|
||||
|
||||
# Информация о заборе
|
||||
fence_info = models.TextField(
|
||||
verbose_name="Информация о заборе", help_text="Введите информацию о заборе"
|
||||
)
|
||||
|
||||
# Согласие на политику конфиденциальности
|
||||
privacy = models.BooleanField(
|
||||
verbose_name="Политика конфиденциальности",
|
||||
help_text="Отметьте, если пользователь согласен с политикой конфиденциальности",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.name} {self.phone} {self.date}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Пользовательский запрос"
|
||||
verbose_name_plural = "Пользовательские запросы"
|
||||
|
||||
|
||||
class SocialNetwork(models.Model):
|
||||
"""
|
||||
Модель SocialNetwork содержит информацию о социальных сетях, включая название, ссылку и иконку.
|
||||
"""
|
||||
|
||||
# Название социальной сети
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name="Название",
|
||||
help_text="Введите название социальной сети",
|
||||
)
|
||||
|
||||
# Ссылка на социальную сеть
|
||||
link = models.CharField(
|
||||
max_length=50,
|
||||
verbose_name="Ссылка",
|
||||
help_text="Введите ссылку на социальную сеть",
|
||||
)
|
||||
|
||||
# Иконка социальной сети (необязательное поле)
|
||||
icon = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Иконка",
|
||||
help_text="Введите HTML-код или путь к иконке социальной сети",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Социальная сеть"
|
||||
verbose_name_plural = "Социальные сети"
|
||||
|
||||
|
||||
class LetterTemplate(models.Model):
|
||||
"""
|
||||
Модель LetterTemplate содержит информацию о шаблонах писем, включая тему, тело письма, копию адресата и тип письма.
|
||||
"""
|
||||
|
||||
# Тема письма
|
||||
subject = models.CharField(
|
||||
max_length=100, verbose_name="Тема письма", help_text="Введите тему письма"
|
||||
)
|
||||
|
||||
# Тело письма
|
||||
body = models.TextField(
|
||||
verbose_name="Текст письма", help_text="Введите текст письма"
|
||||
)
|
||||
|
||||
# Копия адресата (необязательное поле)
|
||||
copy_to = models.EmailField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Копия адресата",
|
||||
help_text="Введите email для копии письма",
|
||||
)
|
||||
|
||||
# Тип письма (выбор из списка)
|
||||
type = models.CharField(
|
||||
choices=[
|
||||
("user_submit", "Submit"),
|
||||
],
|
||||
unique=True,
|
||||
verbose_name="Тип письма",
|
||||
help_text="Выберите тип письма",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Возвращает строковое представление объекта модели.
|
||||
"""
|
||||
return f"{self.subject}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Шаблон письма"
|
||||
verbose_name_plural = "Шаблоны писем"
|
||||
|
|
|
@ -1,28 +1,10 @@
|
|||
from rest_framework import serializers
|
||||
from .models import (
|
||||
AdvantageModel,
|
||||
CalcModel,
|
||||
CustomRequest,
|
||||
FooterModel,
|
||||
KupiZabor,
|
||||
LetterTemplate,
|
||||
PageModel,
|
||||
ReviewModel,
|
||||
Seo,
|
||||
SocialNetwork,
|
||||
Menu,
|
||||
AdvantageModel
|
||||
)
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("root")
|
||||
|
||||
|
||||
class SeoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Seo
|
||||
fields = ["id", "text", "email", "blank", "image"]
|
||||
|
||||
|
||||
class KPSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -30,63 +12,13 @@ class KPSerializer(serializers.ModelSerializer):
|
|||
fields = "__all__"
|
||||
|
||||
|
||||
class PageSerializer(serializers.ModelSerializer):
|
||||
image = serializers.ImageField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = PageModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class MenuSerializer(serializers.ModelSerializer):
|
||||
pages = PageSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = "__all__"
|
||||
depth = 2
|
||||
|
||||
|
||||
class ReviewSerializer(serializers.ModelSerializer):
|
||||
image = serializers.ImageField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = ReviewModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CalcSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CalcModel
|
||||
fields = "__all__"
|
||||
depth = 2
|
||||
|
||||
|
||||
class AdvantageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AdvantageModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class FooterSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FooterModel
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CustomRequestSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomRequest
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SocialNetworkSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SocialNetwork
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class LetterTemplateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = LetterTemplate
|
||||
fields = "__all__"
|
||||
|
|
|
@ -1,51 +1,18 @@
|
|||
import re
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from django.conf import settings
|
||||
from rest_framework import viewsets, filters
|
||||
from rest_framework import viewsets
|
||||
|
||||
from mns.logger import logger
|
||||
|
||||
from .models import (
|
||||
AdvantageModel,
|
||||
CalcModel,
|
||||
CustomRequest,
|
||||
FooterModel,
|
||||
KupiZabor,
|
||||
LetterTemplate,
|
||||
PageModel,
|
||||
ReviewModel,
|
||||
Seo,
|
||||
SocialNetwork,
|
||||
Menu,
|
||||
AdvantageModel
|
||||
)
|
||||
from .serializers import (
|
||||
AdvantageSerializer,
|
||||
CalcSerializer,
|
||||
CustomRequestSerializer,
|
||||
FooterSerializer,
|
||||
KPSerializer,
|
||||
LetterTemplateSerializer,
|
||||
PageSerializer,
|
||||
ReviewSerializer,
|
||||
SeoSerializer,
|
||||
SocialNetworkSerializer,
|
||||
MenuSerializer,
|
||||
AdvantageSerializer
|
||||
)
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("root")
|
||||
|
||||
|
||||
# Create your views here.
|
||||
class SeoViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows partners to be viewed or edited.
|
||||
"""
|
||||
|
||||
http_method_names = ["get"]
|
||||
queryset = Seo.objects.all()
|
||||
serializer_class = SeoSerializer
|
||||
|
||||
|
||||
class KPViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
|
@ -53,118 +20,12 @@ class KPViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = KPSerializer
|
||||
|
||||
|
||||
class PageViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = PageModel.objects.all()
|
||||
serializer_class = PageSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
lookup_field = 'slug'
|
||||
|
||||
|
||||
class MenuViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = Menu.objects.all()
|
||||
serializer_class = MenuSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
|
||||
|
||||
class ReviewViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = ReviewModel.objects.all()
|
||||
serializer_class = ReviewSerializer
|
||||
|
||||
|
||||
class CalcViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = CalcModel.objects.all()
|
||||
serializer_class = CalcSerializer
|
||||
|
||||
|
||||
class AdvantageViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = AdvantageModel.objects.all()
|
||||
serializer_class = AdvantageSerializer
|
||||
|
||||
|
||||
class FooterViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = FooterModel.objects.all()
|
||||
serializer_class = FooterSerializer
|
||||
|
||||
|
||||
class CustomRequestViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["options", "post"]
|
||||
authentication_classes = ()
|
||||
queryset = CustomRequest.objects.all()
|
||||
serializer_class = CustomRequestSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
name = self.request.data.get("name")
|
||||
phone = self.request.data.get("phone")
|
||||
email = self.request.data.get("email")
|
||||
fence_info = self.request.data.get("fence_info")
|
||||
total = re.findall(r"Итого.*?(\d+\s?\d+,?\d+).*?₽", fence_info)
|
||||
total_price = 0 if not len(total) else re.sub(r"\s", "", "" or total[0])
|
||||
data = {
|
||||
"fields[TITLE]": f"КУПИЗАБОР {name} {email} {phone}",
|
||||
"fields[NAME]": name,
|
||||
"fields[STATUS_ID]": "NEW",
|
||||
"fields[OPENED]": "Y",
|
||||
# "fields[ASSIGNED_BY_ID]": "34",
|
||||
"fields[PHONE][0][VALUE]": phone,
|
||||
"fields[PHONE][0][VALUE_TYPE]": "WORK",
|
||||
"fields[EMAIL][0][VALUE]": email,
|
||||
"fields[EMAIL][0][VALUE_TYPE]": "WORK",
|
||||
"fields[SOURCE_ID]": "1",
|
||||
"fields[OPPORTUNITY]": total_price,
|
||||
"fields[UF_CRM_1713510698]": fence_info,
|
||||
"fields[UF_CRM_1712154805]": 47,
|
||||
"fields[UF_CRM_1712154416]": datetime.now() + timedelta(days=1),
|
||||
}
|
||||
bx_result = requests.post(
|
||||
f"{settings.BX_WEBHOOK}/crm.lead.add.json",
|
||||
data,
|
||||
)
|
||||
bx_result_json = bx_result.json()
|
||||
if bx_result_json["result"]:
|
||||
logger.info(f'Был создан лид {bx_result_json["result"]}')
|
||||
letter_template = LetterTemplate.objects.get(type="user_submit")
|
||||
if email:
|
||||
todo_data = {
|
||||
"fields[OWNER_TYPE_ID]": 1,
|
||||
"fields[OWNER_ID]": bx_result_json["result"],
|
||||
"fields[TYPE_ID]": 4,
|
||||
"fields[SUBJECT]": (
|
||||
"КУПИЗАБОР" if not letter_template else letter_template.subject
|
||||
),
|
||||
"fields[DESCRIPTION]": (
|
||||
f"Данные расчета\n{fence_info}"
|
||||
if not letter_template
|
||||
else re.sub("\[fence\]", fence_info, letter_template.body)
|
||||
),
|
||||
"fields[DESCRIPTION_TYPE]": 2,
|
||||
"fields[COMMUNICATIONS][0][VALUE]": email,
|
||||
"fields[COMPLETED]": "Y",
|
||||
"fields[DIRECTION]": 2,
|
||||
"fields[SETTINGS][MESSAGE_FROM]": "СВС технологии <office@svs-tech.pro>",
|
||||
}
|
||||
bx_result_email = requests.post(
|
||||
f"{settings.BX_WEBHOOK}/crm.activity.add",
|
||||
todo_data,
|
||||
)
|
||||
email_result = bx_result_email.json()
|
||||
if email_result:
|
||||
logger.info(f"Было отправлено письмо {email_result['result']}")
|
||||
|
||||
|
||||
class SocialNetworkViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = SocialNetwork.objects.all()
|
||||
serializer_class = SocialNetworkSerializer
|
||||
|
||||
|
||||
class LetterTemplatesViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ["get"]
|
||||
queryset = LetterTemplate.objects.all()
|
||||
serializer_class = LetterTemplateSerializer
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("root")
|
|
@ -50,6 +50,7 @@ CSRF_TRUSTED_ORIGINS = [
|
|||
INSTALLED_APPS = [
|
||||
"corsheaders",
|
||||
"rest_framework",
|
||||
"global_data",
|
||||
"kupizabor",
|
||||
"markdownfield",
|
||||
"django.contrib.admin",
|
||||
|
|
23
mns/urls.py
23
mns/urls.py
|
@ -20,20 +20,21 @@ from django.urls import include, path
|
|||
from django.conf.urls.static import static
|
||||
from rest_framework import routers
|
||||
|
||||
from kupizabor import views
|
||||
from kupizabor import views as kp_views
|
||||
from global_data import views as gd_views
|
||||
|
||||
site_prefix = "kp"
|
||||
router = routers.DefaultRouter()
|
||||
router.register(f"{site_prefix}/seo", views.SeoViewSet)
|
||||
router.register(f"{site_prefix}/kp", views.KPViewSet)
|
||||
router.register(f"{site_prefix}/pages", views.PageViewSet)
|
||||
router.register(f"{site_prefix}/menu", views.MenuViewSet)
|
||||
router.register(f"{site_prefix}/review", views.ReviewViewSet)
|
||||
router.register(f"{site_prefix}/calculator", views.CalcViewSet)
|
||||
router.register(f"{site_prefix}/advantage", views.AdvantageViewSet)
|
||||
router.register(f"{site_prefix}/footer", views.FooterViewSet)
|
||||
router.register(f"{site_prefix}/custom_request", views.CustomRequestViewSet)
|
||||
router.register(f"{site_prefix}/social_network", views.SocialNetworkViewSet)
|
||||
router.register(f"{site_prefix}/seo", gd_views.SeoViewSet)
|
||||
router.register(f"{site_prefix}/kp", kp_views.KPViewSet)
|
||||
router.register(f"{site_prefix}/pages", gd_views.PageViewSet)
|
||||
router.register(f"{site_prefix}/menu", gd_views.MenuViewSet)
|
||||
router.register(f"{site_prefix}/review", gd_views.ReviewViewSet)
|
||||
router.register(f"{site_prefix}/calculator", kp_views.CalcViewSet)
|
||||
router.register(f"{site_prefix}/advantage", kp_views.AdvantageViewSet)
|
||||
router.register(f"{site_prefix}/footer", gd_views.FooterViewSet)
|
||||
router.register(f"{site_prefix}/custom_request", gd_views.CustomRequestViewSet)
|
||||
router.register(f"{site_prefix}/social_network", gd_views.SocialNetworkViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
from mns.logger import logger
|
||||
|
||||
def group_based_upload_to(instance, filename):
|
||||
logger.info(instance)
|
||||
return "files/image/{}/{}/{}".format(
|
||||
type(instance).__name__.lower(), instance.id, filename
|
||||
)
|
Loading…
Reference in New Issue