5c3c27e0af
Раздел «Справка» (категории/статьи/предложка), правки ad_connector и обновление тестов feed под обязательную 2FA. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
222 lines
7.6 KiB
Python
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',
|
|
])
|