documents: права view/edit_all_documents для группы и сотрудника.
Чтобы на портале можно было выдать доступ ко всем документам так же, как к отчётам всех отделов. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -105,6 +105,16 @@ urlpatterns = [
|
||||
user_groups_portal.user_group_individual_reports_revoke_view,
|
||||
name='user_group_individual_reports_revoke',
|
||||
),
|
||||
path(
|
||||
'user-groups/individual-documents/',
|
||||
user_groups_portal.user_group_individual_documents_grant_view,
|
||||
name='user_group_individual_documents_grant',
|
||||
),
|
||||
path(
|
||||
'user-groups/individual-documents/<int:employee_id>/revoke/',
|
||||
user_groups_portal.user_group_individual_documents_revoke_view,
|
||||
name='user_group_individual_documents_revoke',
|
||||
),
|
||||
path('user-groups/<int:group_id>/access/', user_groups_portal.user_group_access_view, name='user_group_access'),
|
||||
path('user-groups/<int:group_id>/access/save/', user_groups_portal.user_group_access_save_view, name='user_group_access_save'),
|
||||
path('user-groups/<int:group_id>/access/copy/', user_groups_portal.user_group_access_copy_view, name='user_group_access_copy'),
|
||||
|
||||
@@ -22,11 +22,15 @@ from apps.visit_manager.models import WorkSchedule, AttendanceLog
|
||||
def _portal_permissions_qs():
|
||||
ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False)
|
||||
access = Permission.objects.filter(content_type=ct, codename__startswith='access_')
|
||||
extra = Permission.objects.filter(
|
||||
reports = Permission.objects.filter(
|
||||
content_type__app_label='reports',
|
||||
codename='view_all_departments',
|
||||
)
|
||||
return (access | extra).order_by('content_type__app_label', 'codename')
|
||||
documents = Permission.objects.filter(
|
||||
content_type__app_label='nextcloud_connector',
|
||||
codename__in=('view_all_documents', 'edit_all_documents'),
|
||||
)
|
||||
return (access | reports | documents).order_by('content_type__app_label', 'codename')
|
||||
|
||||
|
||||
def _pnms_permissions_qs():
|
||||
|
||||
@@ -22,7 +22,11 @@ def can_access_pharmacy_docs(user, pharmacy) -> bool:
|
||||
"""Кто вправе видеть документы конкретной аптеки (список, скачать, OnlyOffice view).
|
||||
|
||||
Политика из PharmacyCardVisibility.show_documents (админка).
|
||||
Глобальне право view/edit_all_documents відкриває всі аптеки.
|
||||
"""
|
||||
from apps.nextcloud_connector.doc_acl import LEVEL_VIEW, user_documents_global_level
|
||||
if user_documents_global_level(user) >= LEVEL_VIEW:
|
||||
return bool(pharmacy and getattr(pharmacy, 'pk', None))
|
||||
from apps.locations.models.pharmacy_visibility import (
|
||||
PharmacyCardVisibility, user_can_view_pharmacy_section,
|
||||
)
|
||||
@@ -33,8 +37,12 @@ def can_access_pharmacy_docs(user, pharmacy) -> bool:
|
||||
def can_edit_pharmacy_docs(user, pharmacy) -> bool:
|
||||
"""Кто вправе менять документы аптеки (upload/mkdir/rename/delete, OnlyOffice edit).
|
||||
|
||||
Совпадает с правом редактирования карточки (PharmacyCardVisibility.edit_pharmacy).
|
||||
Совпадает с правом редактирования карточки (PharmacyCardVisibility.edit_pharmacy),
|
||||
або edit_all_documents.
|
||||
"""
|
||||
from apps.nextcloud_connector.doc_acl import LEVEL_EDIT, user_documents_global_level
|
||||
if user_documents_global_level(user) >= LEVEL_EDIT:
|
||||
return bool(pharmacy and getattr(pharmacy, 'pk', None))
|
||||
from apps.locations.models.pharmacy_visibility import user_can_edit_pharmacy
|
||||
return user_can_edit_pharmacy(user, pharmacy)
|
||||
|
||||
@@ -145,7 +153,12 @@ def accessible_sections(user) -> list[dict]:
|
||||
if not getattr(user, 'is_authenticated', False):
|
||||
return []
|
||||
is_staff = bool(user.is_staff or user.is_superuser)
|
||||
from apps.nextcloud_connector.doc_acl import (
|
||||
LEVEL_EDIT, LEVEL_VIEW, user_documents_global_level,
|
||||
)
|
||||
from apps.nextcloud_connector.services.sync_service import DEPT_ROOT
|
||||
global_lvl = user_documents_global_level(user)
|
||||
can_write_global = is_staff or global_lvl >= LEVEL_EDIT
|
||||
sections = []
|
||||
if user_has_mine_tab(user):
|
||||
sections.append({
|
||||
@@ -155,13 +168,17 @@ def accessible_sections(user) -> list[dict]:
|
||||
'writable': False, # факт записи — по ACL своего отдела
|
||||
})
|
||||
sections.extend([
|
||||
{'key': 'company', 'label': 'Компания', 'relpath': '01_Компания', 'writable': is_staff},
|
||||
{'key': 'org', 'label': 'Структура', 'relpath': DEPT_ROOT, 'writable': is_staff},
|
||||
{'key': 'training', 'label': 'Обучение', 'relpath': '05_Обучение', 'writable': is_staff},
|
||||
{'key': 'company', 'label': 'Компания', 'relpath': '01_Компания', 'writable': can_write_global},
|
||||
{'key': 'org', 'label': 'Структура', 'relpath': DEPT_ROOT, 'writable': can_write_global},
|
||||
{'key': 'training', 'label': 'Обучение', 'relpath': '05_Обучение', 'writable': can_write_global},
|
||||
])
|
||||
if user.is_superuser:
|
||||
sections.append({'key': 'hr', 'label': 'Кадровые дела',
|
||||
'relpath': '04_Кадровые дела', 'writable': True})
|
||||
if user.is_superuser or global_lvl >= LEVEL_VIEW:
|
||||
sections.append({
|
||||
'key': 'hr',
|
||||
'label': 'Кадровые дела',
|
||||
'relpath': '04_Кадровые дела',
|
||||
'writable': bool(user.is_superuser or global_lvl >= LEVEL_EDIT),
|
||||
})
|
||||
return sections
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,31 @@ LEVEL_CODE = {
|
||||
}
|
||||
LEVEL_NAME = {LEVEL_VIEW: 'view', LEVEL_EDIT: 'edit'}
|
||||
|
||||
# Глобальний доступ (група / індивідуально) — як locations.view_all_locations.
|
||||
VIEW_ALL_DOCUMENTS_PERM = 'nextcloud_connector.view_all_documents'
|
||||
EDIT_ALL_DOCUMENTS_PERM = 'nextcloud_connector.edit_all_documents'
|
||||
|
||||
|
||||
def user_documents_global_level(user) -> int:
|
||||
"""Рівень «усі документи» з права Django (група або override).
|
||||
|
||||
staff/superuser тут не обробляємо — їх уже покриває resolve_access.
|
||||
"""
|
||||
if not user or not getattr(user, 'is_authenticated', False):
|
||||
return LEVEL_NONE
|
||||
preview = getattr(user, '_access_preview_perms', None)
|
||||
if preview is not None:
|
||||
if EDIT_ALL_DOCUMENTS_PERM in preview:
|
||||
return LEVEL_EDIT
|
||||
if VIEW_ALL_DOCUMENTS_PERM in preview:
|
||||
return LEVEL_VIEW
|
||||
return LEVEL_NONE
|
||||
if user.has_perm(EDIT_ALL_DOCUMENTS_PERM):
|
||||
return LEVEL_EDIT
|
||||
if user.has_perm(VIEW_ALL_DOCUMENTS_PERM):
|
||||
return LEVEL_VIEW
|
||||
return LEVEL_NONE
|
||||
|
||||
|
||||
def normalize_path(path: str | None) -> str:
|
||||
return (path or '').strip().strip('/')
|
||||
@@ -145,6 +170,8 @@ def resolve_access(user, *, section_key: str, path: str = '',
|
||||
if user.is_superuser or user.is_staff:
|
||||
return AccessDecision(LEVEL_EDIT)
|
||||
|
||||
global_lvl = user_documents_global_level(user)
|
||||
|
||||
path = normalize_path(path)
|
||||
prefixes = path_prefixes(path)
|
||||
|
||||
@@ -160,9 +187,14 @@ def resolve_access(user, *, section_key: str, path: str = '',
|
||||
elif section_key in ('company', 'training'):
|
||||
# читання всім з доступом до розділу, запис — staff (вже вище) або грант
|
||||
default = LEVEL_VIEW
|
||||
elif section_key == 'hr' and global_lvl >= LEVEL_VIEW:
|
||||
default = global_lvl
|
||||
else:
|
||||
default = LEVEL_NONE
|
||||
|
||||
if global_lvl > default:
|
||||
default = global_lvl
|
||||
|
||||
from apps.nextcloud_connector.models import DocumentAccessGrant
|
||||
|
||||
qs = DocumentAccessGrant.objects.filter(
|
||||
@@ -185,6 +217,9 @@ def resolve_access(user, *, section_key: str, path: str = '',
|
||||
if plen > best_len or (plen == best_len and lvl > best):
|
||||
best_len = plen
|
||||
best = lvl
|
||||
# Глобальне view/edit_all_documents — підлога, шляховий грант не знижує.
|
||||
if global_lvl > best:
|
||||
best = global_lvl
|
||||
return AccessDecision(best)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 6.0.5 on 2026-10-23 11:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('nextcloud_connector', '0002_document_access_grant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='documentaccessgrant',
|
||||
options={
|
||||
'ordering': ['section_key', 'department_id', 'path', 'level'],
|
||||
'permissions': [
|
||||
(
|
||||
'view_all_documents',
|
||||
'Перегляд усіх документів (усі відділи, розділи, аптеки)',
|
||||
),
|
||||
(
|
||||
'edit_all_documents',
|
||||
'Редагування усіх документів (усі відділи, розділи, аптеки)',
|
||||
),
|
||||
],
|
||||
'verbose_name': 'Право на документ',
|
||||
'verbose_name_plural': 'Права на документи',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -70,7 +70,20 @@ class DocumentAccessGrant(BaseModel):
|
||||
verbose_name_plural = _('Права на документи')
|
||||
ordering = ['section_key', 'department_id', 'path', 'level']
|
||||
indexes = [
|
||||
models.Index(fields=['section_key', 'department', 'path']),
|
||||
models.Index(
|
||||
fields=['section_key', 'department', 'path'],
|
||||
name='nextcloud_c_section_7c2a1d_idx',
|
||||
),
|
||||
]
|
||||
permissions = [
|
||||
(
|
||||
'view_all_documents',
|
||||
_('Перегляд усіх документів (усі відділи, розділи, аптеки)'),
|
||||
),
|
||||
(
|
||||
'edit_all_documents',
|
||||
_('Редагування усіх документів (усі відділи, розділи, аптеки)'),
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -847,3 +847,80 @@ class BlankOfficeCreateTests(TestCase):
|
||||
self.assertIn('wordprocessingml', office_content_type('a.docx'))
|
||||
self.assertIn('spreadsheetml', office_content_type('a.xlsx'))
|
||||
|
||||
|
||||
class DocumentGlobalAccessTests(TestCase):
|
||||
"""view_all_documents / edit_all_documents — як view_all_reports для документів."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
dtype = DirectoryType.objects.create(code='department', name='Відділ')
|
||||
cls.dept_a = Directory.objects.create(directory_type=dtype, code='it', name='IT')
|
||||
cls.dept_b = Directory.objects.create(directory_type=dtype, code='hr', name='HR')
|
||||
org_type = DirectoryType.objects.create(code='organization', name='Орг')
|
||||
region_type = DirectoryType.objects.create(code='region', name='Регион')
|
||||
region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
|
||||
org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС')
|
||||
cls.ph_a = Pharmacy.objects.create(
|
||||
organization=org, location_region=region,
|
||||
pharmacy_number=71, location_type='pharmacy',
|
||||
)
|
||||
cls.ph_b = Pharmacy.objects.create(
|
||||
organization=org, location_region=region,
|
||||
pharmacy_number=72, location_type='pharmacy',
|
||||
)
|
||||
common = dict(
|
||||
mattermost_sync_enabled=False, ad_sync_enabled=False,
|
||||
hire_date=datetime.date(2020, 1, 1),
|
||||
user_location_type='office',
|
||||
department=cls.dept_a,
|
||||
)
|
||||
cls.user = Employee.objects.create_user(username='docs_global', **common)
|
||||
cls.view_perm = Permission.objects.get(
|
||||
content_type__app_label='nextcloud_connector',
|
||||
codename='view_all_documents',
|
||||
)
|
||||
cls.edit_perm = Permission.objects.get(
|
||||
content_type__app_label='nextcloud_connector',
|
||||
codename='edit_all_documents',
|
||||
)
|
||||
|
||||
def test_view_all_opens_other_dept_and_hr_and_pharmacies(self):
|
||||
from apps.nextcloud_connector.access import accessible_sections
|
||||
from apps.nextcloud_connector.doc_acl import resolve_access
|
||||
|
||||
self.assertFalse(
|
||||
resolve_access(
|
||||
self.user, section_key='org', path='', department_id=self.dept_b.pk,
|
||||
).can_view,
|
||||
)
|
||||
self.user.user_permissions.add(self.view_perm)
|
||||
user = Employee.objects.get(pk=self.user.pk)
|
||||
other = resolve_access(
|
||||
user, section_key='org', path='secret.docx', department_id=self.dept_b.pk,
|
||||
)
|
||||
self.assertTrue(other.can_view)
|
||||
self.assertFalse(other.can_edit)
|
||||
hr = resolve_access(user, section_key='hr', path='')
|
||||
self.assertTrue(hr.can_view)
|
||||
self.assertFalse(hr.can_edit)
|
||||
self.assertTrue(can_access_pharmacy_docs(user, self.ph_b))
|
||||
self.assertFalse(can_edit_pharmacy_docs(user, self.ph_b))
|
||||
keys = {s['key'] for s in accessible_sections(user)}
|
||||
self.assertIn('hr', keys)
|
||||
|
||||
def test_edit_all_raises_to_edit(self):
|
||||
from apps.nextcloud_connector.doc_acl import resolve_access
|
||||
|
||||
self.user.user_permissions.add(self.view_perm, self.edit_perm)
|
||||
user = Employee.objects.get(pk=self.user.pk)
|
||||
dec = resolve_access(
|
||||
user, section_key='org', path='', department_id=self.dept_b.pk,
|
||||
)
|
||||
self.assertTrue(dec.can_edit)
|
||||
hr = resolve_access(user, section_key='hr', path='x.docx')
|
||||
self.assertTrue(hr.can_edit)
|
||||
self.assertTrue(can_edit_pharmacy_docs(user, self.ph_a))
|
||||
self.assertTrue(can_edit_pharmacy_docs(user, self.ph_b))
|
||||
|
||||
|
||||
@@ -58,8 +58,14 @@ MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # 100 МБ
|
||||
|
||||
def _get_pharmacy_or_403(request, pk):
|
||||
"""Возвращает (pharmacy, None) или (None, HttpResponseForbidden)."""
|
||||
from apps.locations.models import Pharmacy
|
||||
from apps.nextcloud_connector.doc_acl import LEVEL_VIEW, user_documents_global_level
|
||||
from core.location_access import accessible_pharmacies
|
||||
pharmacy = get_object_or_404(accessible_pharmacies(request.user), pk=pk)
|
||||
|
||||
if user_documents_global_level(request.user) >= LEVEL_VIEW:
|
||||
pharmacy = get_object_or_404(Pharmacy.objects.all(), pk=pk)
|
||||
else:
|
||||
pharmacy = get_object_or_404(accessible_pharmacies(request.user), pk=pk)
|
||||
if not can_access_pharmacy_docs(request.user, pharmacy):
|
||||
return None, HttpResponseForbidden('Нет доступа к документам этой аптеки.')
|
||||
return pharmacy, None
|
||||
@@ -865,11 +871,17 @@ def _resolve_token_relpath(payload: dict) -> tuple[str, str, bool] | None:
|
||||
return None
|
||||
if kind == 'pharmacy':
|
||||
from django.contrib.auth import get_user_model
|
||||
from apps.locations.models import Pharmacy
|
||||
from apps.nextcloud_connector.doc_acl import LEVEL_VIEW, user_documents_global_level
|
||||
from core.location_access import accessible_pharmacies
|
||||
user = get_user_model().objects.filter(pk=payload.get('uid'), is_active=True).first()
|
||||
if user is None:
|
||||
return None
|
||||
pharmacy = accessible_pharmacies(user).filter(pk=payload.get('pharmacy_id')).first()
|
||||
ph_id = payload.get('pharmacy_id')
|
||||
if user_documents_global_level(user) >= LEVEL_VIEW:
|
||||
pharmacy = Pharmacy.objects.filter(pk=ph_id).first()
|
||||
else:
|
||||
pharmacy = accessible_pharmacies(user).filter(pk=ph_id).first()
|
||||
if pharmacy is None or not can_access_pharmacy_docs(user, pharmacy):
|
||||
return None
|
||||
writable = can_edit_pharmacy_docs(user, pharmacy) and can_edit_file(name)
|
||||
|
||||
@@ -48,6 +48,166 @@ def _sync_view_all_reports(group: Group, enabled: bool) -> None:
|
||||
group.permissions.remove(perm)
|
||||
|
||||
|
||||
def _documents_perm(codename: str) -> Permission | None:
|
||||
return Permission.objects.filter(
|
||||
content_type__app_label='nextcloud_connector',
|
||||
codename=codename,
|
||||
).first()
|
||||
|
||||
|
||||
def _view_all_documents_perm() -> Permission | None:
|
||||
return _documents_perm('view_all_documents')
|
||||
|
||||
|
||||
def _edit_all_documents_perm() -> Permission | None:
|
||||
return _documents_perm('edit_all_documents')
|
||||
|
||||
|
||||
def group_can_view_all_documents(group: Group) -> bool:
|
||||
return group.permissions.filter(
|
||||
content_type__app_label='nextcloud_connector',
|
||||
codename='view_all_documents',
|
||||
).exists()
|
||||
|
||||
|
||||
def group_can_edit_all_documents(group: Group) -> bool:
|
||||
return group.permissions.filter(
|
||||
content_type__app_label='nextcloud_connector',
|
||||
codename='edit_all_documents',
|
||||
).exists()
|
||||
|
||||
|
||||
def _sync_documents_global(group: Group, *, view: bool, edit: bool) -> None:
|
||||
"""edit_all передбачає view_all."""
|
||||
view_perm = _view_all_documents_perm()
|
||||
edit_perm = _edit_all_documents_perm()
|
||||
if edit:
|
||||
view = True
|
||||
if view_perm is not None:
|
||||
if view:
|
||||
group.permissions.add(view_perm)
|
||||
else:
|
||||
group.permissions.remove(view_perm)
|
||||
if edit_perm is not None:
|
||||
if edit:
|
||||
group.permissions.add(edit_perm)
|
||||
else:
|
||||
group.permissions.remove(edit_perm)
|
||||
|
||||
|
||||
def _employee_has_perm_or_grant(employee, perm: Permission | None) -> bool:
|
||||
if perm is None:
|
||||
return False
|
||||
if employee.user_permissions.filter(pk=perm.pk).exists():
|
||||
return True
|
||||
return EmployeePermissionOverride.objects.filter(
|
||||
employee=employee,
|
||||
permission=perm,
|
||||
override_type='grant',
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
|
||||
def employee_documents_global_level(employee) -> str | None:
|
||||
"""'edit' | 'view' | None — індивідуальне право на всі документи."""
|
||||
if _employee_has_perm_or_grant(employee, _edit_all_documents_perm()):
|
||||
return 'edit'
|
||||
if _employee_has_perm_or_grant(employee, _view_all_documents_perm()):
|
||||
return 'view'
|
||||
return None
|
||||
|
||||
|
||||
def _set_individual_perm_grant(employee, perm: Permission, *, comment: str) -> None:
|
||||
EmployeePermissionOverride.objects.filter(
|
||||
employee=employee,
|
||||
permission=perm,
|
||||
override_type='revoke',
|
||||
).update(is_active=False)
|
||||
row = EmployeePermissionOverride.objects.filter(
|
||||
employee=employee,
|
||||
permission=perm,
|
||||
override_type='grant',
|
||||
).first()
|
||||
if row:
|
||||
row.is_active = True
|
||||
if not row.comment:
|
||||
row.comment = comment
|
||||
row.save(update_fields=['is_active', 'comment', 'updated_at'])
|
||||
else:
|
||||
EmployeePermissionOverride.objects.create(
|
||||
employee=employee,
|
||||
permission=perm,
|
||||
override_type='grant',
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
|
||||
def _clear_individual_perm(employee, perm: Permission | None) -> None:
|
||||
if perm is None:
|
||||
return
|
||||
EmployeePermissionOverride.objects.filter(
|
||||
employee=employee,
|
||||
permission=perm,
|
||||
override_type='grant',
|
||||
).update(is_active=False)
|
||||
employee.user_permissions.remove(perm)
|
||||
|
||||
|
||||
def grant_individual_documents_global(employee, level: str = 'view') -> None:
|
||||
"""level: 'view' | 'edit'."""
|
||||
view_perm = _view_all_documents_perm()
|
||||
edit_perm = _edit_all_documents_perm()
|
||||
if view_perm is None or edit_perm is None:
|
||||
raise ValueError(
|
||||
'Права «усі документи» ще не створені. Застосуйте міграції nextcloud_connector.'
|
||||
)
|
||||
if level == 'edit':
|
||||
_set_individual_perm_grant(
|
||||
employee, view_perm, comment='Перегляд усіх документів',
|
||||
)
|
||||
_set_individual_perm_grant(
|
||||
employee, edit_perm, comment='Редагування усіх документів',
|
||||
)
|
||||
else:
|
||||
_clear_individual_perm(employee, edit_perm)
|
||||
_set_individual_perm_grant(
|
||||
employee, view_perm, comment='Перегляд усіх документів',
|
||||
)
|
||||
|
||||
|
||||
def revoke_individual_documents_global(employee) -> None:
|
||||
_clear_individual_perm(employee, _view_all_documents_perm())
|
||||
_clear_individual_perm(employee, _edit_all_documents_perm())
|
||||
|
||||
|
||||
def individual_documents_global_employees():
|
||||
view_perm = _view_all_documents_perm()
|
||||
edit_perm = _edit_all_documents_perm()
|
||||
if view_perm is None and edit_perm is None:
|
||||
return []
|
||||
from django.db.models import Q
|
||||
|
||||
q = Q()
|
||||
for perm in (view_perm, edit_perm):
|
||||
if perm is None:
|
||||
continue
|
||||
q |= Q(user_permissions=perm) | Q(
|
||||
permission_overrides__permission=perm,
|
||||
permission_overrides__override_type='grant',
|
||||
permission_overrides__is_active=True,
|
||||
)
|
||||
employees = (
|
||||
Employee.objects.filter(is_active=True)
|
||||
.filter(q)
|
||||
.distinct()
|
||||
.order_by('last_name', 'first_name', 'username')
|
||||
)
|
||||
return [
|
||||
{'employee': emp, 'level': employee_documents_global_level(emp) or 'view'}
|
||||
for emp in employees
|
||||
]
|
||||
|
||||
|
||||
def employee_has_individual_view_all_reports(employee) -> bool:
|
||||
perm = _view_all_reports_perm()
|
||||
if perm is None:
|
||||
@@ -181,6 +341,11 @@ def apply_group_access_matrix(group: Group, posted: dict) -> None:
|
||||
group,
|
||||
posted.get('view_all_reports') in ('1', 'on', 'yes'),
|
||||
)
|
||||
_sync_documents_global(
|
||||
group,
|
||||
view=posted.get('view_all_documents') in ('1', 'on', 'yes'),
|
||||
edit=posted.get('edit_all_documents') in ('1', 'on', 'yes'),
|
||||
)
|
||||
|
||||
|
||||
def full_access_matrix() -> dict:
|
||||
@@ -199,6 +364,20 @@ def full_access_matrix() -> dict:
|
||||
'label': 'Отчёты по всем отделам',
|
||||
'cells': [{'group_id': g.id, 'checked': group_can_view_all_reports(g)} for g in groups],
|
||||
})
|
||||
rows.append({
|
||||
'key': 'view_all_documents',
|
||||
'label': 'Документы: просмотр всех',
|
||||
'cells': [
|
||||
{'group_id': g.id, 'checked': group_can_view_all_documents(g)} for g in groups
|
||||
],
|
||||
})
|
||||
rows.append({
|
||||
'key': 'edit_all_documents',
|
||||
'label': 'Документы: редактирование всех',
|
||||
'cells': [
|
||||
{'group_id': g.id, 'checked': group_can_edit_all_documents(g)} for g in groups
|
||||
],
|
||||
})
|
||||
return {'groups': groups, 'rows': rows}
|
||||
|
||||
|
||||
@@ -223,6 +402,11 @@ def apply_full_access_matrix(posted: dict) -> None:
|
||||
g.permissions.remove(perm)
|
||||
GroupSectionAccess.objects.filter(group=g, section_key=key).delete()
|
||||
_sync_view_all_reports(g, posted.get(f'g{g.id}_view_all_reports') is not None)
|
||||
_sync_documents_global(
|
||||
g,
|
||||
view=posted.get(f'g{g.id}_view_all_documents') is not None,
|
||||
edit=posted.get(f'g{g.id}_edit_all_documents') is not None,
|
||||
)
|
||||
|
||||
|
||||
def copy_group_access(source: Group, target: Group) -> None:
|
||||
@@ -256,6 +440,11 @@ def copy_group_access(source: Group, target: Group) -> None:
|
||||
target.permissions.remove(perm)
|
||||
GroupSectionAccess.objects.filter(group=target, section_key=key).delete()
|
||||
_sync_view_all_reports(target, group_can_view_all_reports(source))
|
||||
_sync_documents_global(
|
||||
target,
|
||||
view=group_can_view_all_documents(source),
|
||||
edit=group_can_edit_all_documents(source),
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -273,6 +462,7 @@ def user_groups_list_view(request):
|
||||
context = {
|
||||
'groups': groups,
|
||||
'individual_report_viewers': list(individual_view_all_report_employees()),
|
||||
'individual_document_grants': individual_documents_global_employees(),
|
||||
'employee_choices': list(
|
||||
Employee.objects.filter(is_active=True)
|
||||
.order_by('last_name', 'first_name', 'username')
|
||||
@@ -316,6 +506,43 @@ def user_group_individual_reports_revoke_view(request, employee_id: int):
|
||||
return redirect('user_groups')
|
||||
|
||||
|
||||
@login_required
|
||||
@section_required('user_groups')
|
||||
@require_POST
|
||||
def user_group_individual_documents_grant_view(request):
|
||||
raw_id = (request.POST.get('employee_id') or '').strip()
|
||||
level = (request.POST.get('level') or 'view').strip().lower()
|
||||
if level not in ('view', 'edit'):
|
||||
level = 'view'
|
||||
if not raw_id.isdigit():
|
||||
request.session['ug_error'] = 'Выберите сотрудника.'
|
||||
return redirect('user_groups')
|
||||
employee = get_object_or_404(Employee, pk=int(raw_id), is_active=True)
|
||||
try:
|
||||
grant_individual_documents_global(employee, level=level)
|
||||
except ValueError as exc:
|
||||
request.session['ug_error'] = str(exc)
|
||||
return redirect('user_groups')
|
||||
label = 'редактировать' if level == 'edit' else 'видеть'
|
||||
request.session['ug_message'] = (
|
||||
f'«{employee.get_full_name() or employee.username}» может {label} все документы.'
|
||||
)
|
||||
return redirect('user_groups')
|
||||
|
||||
|
||||
@login_required
|
||||
@section_required('user_groups')
|
||||
@require_POST
|
||||
def user_group_individual_documents_revoke_view(request, employee_id: int):
|
||||
employee = get_object_or_404(Employee, pk=employee_id)
|
||||
revoke_individual_documents_global(employee)
|
||||
request.session['ug_message'] = (
|
||||
f'Индивидуальный доступ ко всем документам снят у '
|
||||
f'«{employee.get_full_name() or employee.username}».'
|
||||
)
|
||||
return redirect('user_groups')
|
||||
|
||||
|
||||
@login_required
|
||||
@section_required('user_groups')
|
||||
def user_group_access_view(request, group_id: int):
|
||||
@@ -325,6 +552,8 @@ def user_group_access_view(request, group_id: int):
|
||||
'group': group,
|
||||
'matrix_rows': _matrix_rows(group),
|
||||
'view_all_reports': group_can_view_all_reports(group),
|
||||
'view_all_documents': group_can_view_all_documents(group),
|
||||
'edit_all_documents': group_can_edit_all_documents(group),
|
||||
'other_groups': other_groups,
|
||||
'ug_message': request.session.pop('ug_message', ''),
|
||||
'ug_error': request.session.pop('ug_error', ''),
|
||||
|
||||
@@ -92,6 +92,28 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1rem; background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); padding:1rem 1.25rem;">
|
||||
<div style="font-size:0.7rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--color-muted-foreground); margin-bottom:0.5rem;">{% trans "Документы" %}</div>
|
||||
<label style="display:flex; align-items:flex-start; gap:0.6rem; cursor:pointer; font-size:0.875rem; margin-bottom:0.75rem;">
|
||||
<input type="checkbox" name="view_all_documents" value="1" {% if view_all_documents or edit_all_documents %}checked{% endif %} style="margin-top:0.2rem;">
|
||||
<span>
|
||||
<strong>{% trans "Просмотр всех документов" %}</strong>
|
||||
<span style="display:block; font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.2rem;">
|
||||
{% trans "Все отделы, разделы, кадровые дела и документы аптек. Без галочки — только свой отдел и выданные ACL." %}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label style="display:flex; align-items:flex-start; gap:0.6rem; cursor:pointer; font-size:0.875rem;">
|
||||
<input type="checkbox" name="edit_all_documents" value="1" {% if edit_all_documents %}checked{% endif %} style="margin-top:0.2rem;">
|
||||
<span>
|
||||
<strong>{% trans "Редактирование всех документов" %}</strong>
|
||||
<span style="display:block; font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.2rem;">
|
||||
{% trans "Включает просмотр. Загрузка, изменение и удаление во всех разделах и аптеках." %}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:0.75rem; margin-top:1.25rem; flex-wrap:wrap;">
|
||||
<a href="{% url 'user_groups' %}"
|
||||
hx-get="{% url 'user_groups' %}" hx-target="#main-content" hx-push-url="{% url 'user_groups' %}"
|
||||
|
||||
@@ -93,4 +93,53 @@
|
||||
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin:0;">{% trans "Пока никому персонально не выдано." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem; background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); padding:1.25rem;">
|
||||
<h2 style="font-size:0.9375rem; font-weight:500; margin:0 0 0.35rem;">{% trans "Все документы — отдельным сотрудникам" %}</h2>
|
||||
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin:0 0 1rem;">
|
||||
{% trans "Не трогает группу: право только у выбранного человека. Можно выдать просмотр или полное редактирование всех документов." %}
|
||||
</p>
|
||||
<form method="post" action="{% url 'user_group_individual_documents_grant' %}"
|
||||
style="display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; margin:0 0 1rem;">
|
||||
{% csrf_token %}
|
||||
<select name="employee_id" required class="input" style="min-width:18rem;">
|
||||
<option value="">{% trans "Выберите сотрудника…" %}</option>
|
||||
{% for emp in employee_choices %}
|
||||
<option value="{{ emp.id }}">{{ emp.last_name }} {{ emp.first_name }} ({{ emp.username }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="level" class="input" style="min-width:10rem;">
|
||||
<option value="view">{% trans "Просмотр" %}</option>
|
||||
<option value="edit">{% trans "Редактирование" %}</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Выдать доступ" %}</button>
|
||||
</form>
|
||||
{% if individual_document_grants %}
|
||||
<table style="width:100%; border-collapse:collapse; font-size:0.8125rem;">
|
||||
<thead>
|
||||
<tr style="background:var(--color-muted);">
|
||||
<th style="padding:0.5rem 0.75rem; text-align:left; font-size:0.7rem; text-transform:uppercase; color:var(--color-muted-foreground);">{% trans "Сотрудник" %}</th>
|
||||
<th style="padding:0.5rem 0.75rem; text-align:left; font-size:0.7rem; text-transform:uppercase; color:var(--color-muted-foreground);">{% trans "Уровень" %}</th>
|
||||
<th style="padding:0.5rem 0.75rem;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in individual_document_grants %}
|
||||
<tr style="border-top:1px solid var(--color-border);">
|
||||
<td style="padding:0.6rem 0.75rem;">{{ row.employee.last_name }} {{ row.employee.first_name }} <span style="color:var(--color-muted-foreground);">({{ row.employee.username }})</span></td>
|
||||
<td style="padding:0.6rem 0.75rem;">{% if row.level == 'edit' %}{% trans "Редактирование" %}{% else %}{% trans "Просмотр" %}{% endif %}</td>
|
||||
<td style="padding:0.6rem 0.75rem; text-align:right;">
|
||||
<form method="post" action="{% url 'user_group_individual_documents_revoke' row.employee.id %}" style="margin:0;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-secondary" style="font-size:0.75rem;">{% trans "Снять" %}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin:0;">{% trans "Пока никому персонально не выдано." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user