Files
ACORE/apps/help/models.py
T
lokses 5c3c27e0af help + ad_connector + feed: база знаний и правки тестов
Раздел «Справка» (категории/статьи/предложка), правки ad_connector
и обновление тестов feed под обязательную 2FA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:03:55 +03:00

222 lines
7.6 KiB
Python

from __future__ import annotations
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from core.models.base import BaseModel
from apps.help.sanitize import clean_html
def _unique_slug(model, base: str, exclude_pk=None) -> str:
raw = slugify(base, allow_unicode=True) or 'article'
slug = raw[:180]
n = 2
qs = model.objects.all()
if exclude_pk:
qs = qs.exclude(pk=exclude_pk)
while qs.filter(slug=slug).exists():
suffix = f'-{n}'
slug = f'{raw[:180 - len(suffix)]}{suffix}'
n += 1
return slug
class HelpCategory(BaseModel):
slug = models.SlugField(_('Slug'), max_length=80, unique=True, allow_unicode=True)
title = models.CharField(_('Название'), max_length=120)
description = models.CharField(_('Описание'), max_length=255, blank=True)
icon = models.CharField(
_('Иконка'),
max_length=32,
default='book',
help_text=_('Код иконки: user, hr, docs, tasks, security, support, book'),
)
sort_order = models.PositiveIntegerField(_('Порядок'), default=0, db_index=True)
is_active = models.BooleanField(_('Активна'), default=True, db_index=True)
class Meta:
verbose_name = _('Категория справки')
verbose_name_plural = _('Категории справки')
ordering = ['sort_order', 'title']
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = _unique_slug(HelpCategory, self.title, exclude_pk=self.pk)
super().save(*args, **kwargs)
@property
def published_count(self) -> int:
return self.articles.filter(is_published=True).count()
class HelpArticle(BaseModel):
category = models.ForeignKey(
HelpCategory,
on_delete=models.PROTECT,
related_name='articles',
verbose_name=_('Категория'),
)
slug = models.SlugField(_('Slug'), max_length=200, unique=True, allow_unicode=True)
title = models.CharField(_('Заголовок'), max_length=250)
body = models.TextField(_('Текст'))
is_published = models.BooleanField(_('Опубликовано'), default=False, db_index=True)
is_featured = models.BooleanField(
_('Частый вопрос'),
default=False,
db_index=True,
help_text=_('Показывать в блоке «Частые вопросы» на главной справки'),
)
sort_order = models.PositiveIntegerField(_('Порядок'), default=0, db_index=True)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='help_articles',
verbose_name=_('Автор'),
)
class Meta:
verbose_name = _('Статья справки')
verbose_name_plural = _('Статьи справки')
ordering = ['sort_order', 'title']
indexes = [
models.Index(fields=['category', 'is_published']),
models.Index(fields=['is_featured', 'is_published']),
]
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = _unique_slug(HelpArticle, self.title, exclude_pk=self.pk)
self.body = clean_html(self.body)
super().save(*args, **kwargs)
class HelpContribution(BaseModel):
KIND_CREATE = 'create'
KIND_UPDATE = 'update'
KIND_CHOICES = [
(KIND_CREATE, _('Новая статья')),
(KIND_UPDATE, _('Правка статьи')),
]
STATUS_PENDING = 'pending'
STATUS_APPROVED = 'approved'
STATUS_REJECTED = 'rejected'
STATUS_CHOICES = [
(STATUS_PENDING, _('На проверке')),
(STATUS_APPROVED, _('Одобрено')),
(STATUS_REJECTED, _('Отклонено')),
]
kind = models.CharField(_('Тип'), max_length=16, choices=KIND_CHOICES, default=KIND_CREATE)
article = models.ForeignKey(
HelpArticle,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='contributions',
verbose_name=_('Статья'),
help_text=_('Обязательно для правки существующей статьи'),
)
category = models.ForeignKey(
HelpCategory,
on_delete=models.PROTECT,
related_name='contributions',
verbose_name=_('Категория'),
)
title = models.CharField(_('Заголовок'), max_length=250)
body = models.TextField(_('Текст'))
status = models.CharField(
_('Статус'),
max_length=16,
choices=STATUS_CHOICES,
default=STATUS_PENDING,
db_index=True,
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='help_contributions',
verbose_name=_('Автор предложения'),
)
reviewed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='help_reviews',
verbose_name=_('Проверил'),
)
reviewed_at = models.DateTimeField(_('Дата проверки'), null=True, blank=True)
review_note = models.CharField(_('Комментарий модератора'), max_length=500, blank=True)
class Meta:
verbose_name = _('Предложение в справку')
verbose_name_plural = _('Предложения в справку')
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']),
models.Index(fields=['author', '-created_at']),
]
def __str__(self):
return f'{self.get_kind_display()}: {self.title}'
def save(self, *args, **kwargs):
self.body = clean_html(self.body)
super().save(*args, **kwargs)
def apply_approval(self, reviewer) -> HelpArticle:
"""Одобрить: создать/обновить статью и опубликовать."""
if self.status == self.STATUS_APPROVED and self.article_id:
return self.article
body = clean_html(self.body)
if self.kind == self.KIND_UPDATE and self.article_id:
article = self.article
article.category = self.category
article.title = self.title
article.body = body
article.is_published = True
if not article.author_id:
article.author = self.author
article.save()
else:
article = HelpArticle(
category=self.category,
title=self.title,
body=body,
is_published=True,
author=self.author,
)
article.save()
self.article = article
self.status = self.STATUS_APPROVED
self.reviewed_by = reviewer
self.reviewed_at = timezone.now()
self.save(update_fields=[
'article', 'status', 'reviewed_by', 'reviewed_at', 'updated_at',
])
return article
def reject(self, reviewer, note: str = '') -> None:
self.status = self.STATUS_REJECTED
self.reviewed_by = reviewer
self.reviewed_at = timezone.now()
self.review_note = (note or '')[:500]
self.save(update_fields=[
'status', 'reviewed_by', 'reviewed_at', 'review_note', 'updated_at',
])