6af7a210f2
Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
5.5 KiB
Python
135 lines
5.5 KiB
Python
from django.conf import settings
|
||
from django.core.exceptions import ValidationError
|
||
from django.db import models
|
||
from django.utils.translation import gettext_lazy as _
|
||
|
||
from core.models import BaseModel
|
||
|
||
|
||
class DocumentAccessGrant(BaseModel):
|
||
"""
|
||
Право на папку або файл у розділі документів.
|
||
|
||
section_key — company / training / org (дерево відділів).
|
||
department — для org: який вузол оргструктури; path — підшлях у його папці
|
||
('' = увесь відділ; 'секрет/акт.pdf' = лише цей файл).
|
||
"""
|
||
|
||
LEVELS = [
|
||
('view', _('Перегляд')),
|
||
('edit', _('Редагування')),
|
||
]
|
||
|
||
section_key = models.CharField(_('Розділ'), max_length=32, db_index=True)
|
||
department = models.ForeignKey(
|
||
'core.Directory',
|
||
on_delete=models.CASCADE,
|
||
null=True, blank=True,
|
||
related_name='document_access_grants',
|
||
verbose_name=_('Відділ'),
|
||
limit_choices_to={'directory_type__code': 'department'},
|
||
)
|
||
path = models.CharField(
|
||
_('Шлях'), max_length=500, blank=True, default='',
|
||
help_text=_('Відносно кореня розділу/відділу. Порожньо = увесь каталог.'),
|
||
)
|
||
is_folder = models.BooleanField(
|
||
_('Папка'), default=True,
|
||
help_text=_('False — право лише на цей файл (path має містити імʼя файлу).'),
|
||
)
|
||
level = models.CharField(_('Рівень'), max_length=8, choices=LEVELS, default='view')
|
||
|
||
employee = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
null=True, blank=True,
|
||
related_name='document_access_grants',
|
||
verbose_name=_('Співробітник'),
|
||
)
|
||
grant_department = models.ForeignKey(
|
||
'core.Directory',
|
||
on_delete=models.CASCADE,
|
||
null=True, blank=True,
|
||
related_name='document_access_grants_as_subject',
|
||
verbose_name=_('Відділ-отримувач'),
|
||
limit_choices_to={'directory_type__code': 'department'},
|
||
help_text=_('Усім співробітникам цього відділу.'),
|
||
)
|
||
group = models.ForeignKey(
|
||
'auth.Group',
|
||
on_delete=models.CASCADE,
|
||
null=True, blank=True,
|
||
related_name='document_access_grants',
|
||
verbose_name=_('Група'),
|
||
)
|
||
is_active = models.BooleanField(_('Активне'), default=True)
|
||
note = models.CharField(_('Примітка'), max_length=255, blank=True)
|
||
|
||
class Meta:
|
||
verbose_name = _('Право на документ')
|
||
verbose_name_plural = _('Права на документи')
|
||
ordering = ['section_key', 'department_id', 'path', 'level']
|
||
indexes = [
|
||
models.Index(fields=['section_key', 'department', 'path']),
|
||
]
|
||
|
||
def __str__(self):
|
||
target = self.path or '«корінь»'
|
||
who = (
|
||
self.employee or self.grant_department or self.group
|
||
or '?'
|
||
)
|
||
return f'{self.section_key}:{target} → {who} ({self.level})'
|
||
|
||
def clean(self):
|
||
subjects = [self.employee_id, self.grant_department_id, self.group_id]
|
||
if sum(1 for s in subjects if s) != 1:
|
||
raise ValidationError(
|
||
_('Вкажіть рівно одного отримувача: співробітник, відділ або група.'),
|
||
)
|
||
self.path = (self.path or '').strip().strip('/')
|
||
|
||
def save(self, *args, **kwargs):
|
||
self.path = (self.path or '').strip().strip('/')
|
||
super().save(*args, **kwargs)
|
||
|
||
|
||
class NextcloudSyncLog(BaseModel):
|
||
"""Журнал провижининга Group Folders в Nextcloud.
|
||
|
||
В отличие от AD/Carbonio драйвер тут не один: папки заводятся и под отделы
|
||
(Directory type=department), и под аптеки (Pharmacy). Поэтому лог обобщённый —
|
||
entity_type + entity_ref, а не FK на конкретную модель.
|
||
"""
|
||
|
||
ENTITY_TYPES = [
|
||
('department', _('Отдел')),
|
||
('pharmacy', _('Аптека')),
|
||
]
|
||
ACTIONS = [
|
||
('create', _('Создание папки')),
|
||
('update', _('Обновление ACL')),
|
||
('skip', _('Пропуск')),
|
||
]
|
||
STATUSES = [
|
||
('success', _('Успех')),
|
||
('error', _('Ошибка')),
|
||
('dry_run', _('Пробный прогон')),
|
||
]
|
||
|
||
entity_type = models.CharField(_('Тип сущности'), max_length=20, choices=ENTITY_TYPES)
|
||
entity_ref = models.CharField(_('Ссылка на сущность'), max_length=255,
|
||
help_text=_('Код отдела или номер аптеки'))
|
||
mountpoint = models.CharField(_('Путь Group Folder'), max_length=255, blank=True)
|
||
action = models.CharField(_('Действие'), max_length=20, choices=ACTIONS)
|
||
status = models.CharField(_('Статус'), max_length=10, choices=STATUSES)
|
||
message = models.TextField(_('Сообщение'), blank=True)
|
||
|
||
class Meta:
|
||
verbose_name = _('Запись синхронизации Nextcloud')
|
||
verbose_name_plural = _('Журнал синхронизации Nextcloud')
|
||
ordering = ['-created_at']
|
||
|
||
def __str__(self):
|
||
return f'{self.get_entity_type_display()} {self.entity_ref} — {self.get_action_display()} — {self.get_status_display()}'
|