Hide peer logins on profiles and restrict view_all to published reports.

Login stays visible only to self/manager/HR; draft report docs stay department-head only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-14 15:20:49 +03:00
parent 555dbfb157
commit bf5310e489
5 changed files with 62 additions and 13 deletions
+23 -11
View File
@@ -29,22 +29,25 @@ def user_can_open_reports_hub(user) -> bool:
def user_can_view_report_document(user, doc: ReportDocument) -> bool:
if not user_can_open_reports_hub(user):
return False
# Черновики — только руководителю своего отдела.
if user_can_manage_it_monthly_report(user) and getattr(user, 'department_id', None) == doc.department_id:
return True
# view_all и обычные сотрудники — только опубликованные.
if not doc.is_published:
return False
if user_can_view_all_department_reports(user):
return True
if getattr(user, 'department_id', None) != doc.department_id:
return False
if user_can_manage_it_monthly_report(user):
return True
return doc.is_published
return getattr(user, 'department_id', None) == doc.department_id
def user_can_edit_report_document(user, doc: ReportDocument) -> bool:
if not user_can_open_reports_hub(user):
return False
if user_can_view_all_department_reports(user):
if user_can_manage_it_monthly_report(user) and getattr(user, 'department_id', None) == doc.department_id:
return True
if user_can_manage_it_monthly_report(user):
return getattr(user, 'department_id', None) == doc.department_id
# view_all может править/снимать только уже опубликованные (черновики чужих отделов не видит).
if user_can_view_all_department_reports(user):
return doc.is_published
return False
@@ -65,13 +68,22 @@ def visible_report_documents(user):
if not user_can_open_reports_hub(user):
return ReportDocument.objects.none()
qs = ReportDocument.objects.select_related('department', 'created_by', 'published_by')
if user_can_view_all_department_reports(user):
return qs
dept_id = getattr(user, 'department_id', None)
manage_own = user_can_manage_it_monthly_report(user) and bool(dept_id)
if user_can_view_all_department_reports(user):
# Все отделы — только опубликованные; свои черновики — если ещё и руководитель.
if manage_own:
from django.db.models import Q
return qs.filter(
Q(status=ReportDocument.STATUS_PUBLISHED) | Q(department_id=dept_id)
).distinct()
return qs.filter(status=ReportDocument.STATUS_PUBLISHED)
if not dept_id:
return ReportDocument.objects.none()
qs = qs.filter(department_id=dept_id)
if user_can_manage_it_monthly_report(user):
if manage_own:
return qs
return qs.filter(status=ReportDocument.STATUS_PUBLISHED)
+36
View File
@@ -643,3 +643,39 @@ class ReportDocumentTests(TestCase):
fake = SimpleUploadedFile('x.docx', b'x')
r = self.client.post('/reports/documents/upload/', {'file': fake})
self.assertEqual(r.status_code, 403)
def test_view_all_sees_only_published(self):
from django.contrib.auth.models import Permission
from apps.reports.documents import visible_report_documents
from apps.reports.models import ReportDocument
other_dept_type, _ = DirectoryType.objects.get_or_create(
code='department', defaults={'name': 'Відділи', 'hierarchy_type': 'strict'},
)
other = Directory.objects.create(
directory_type=other_dept_type, code='hr-docs', name='HR-docs',
)
draft = ReportDocument.objects.create(
department=other, title='Черновик HR', filename='draft.docx',
kind=ReportDocument.KIND_EXTRA, status=ReportDocument.STATUS_DRAFT,
created_by=self.head,
)
published = ReportDocument.objects.create(
department=other, title='Опубликован HR', filename='pub.docx',
kind=ReportDocument.KIND_EXTRA, status=ReportDocument.STATUS_PUBLISHED,
created_by=self.head,
)
viewer = make_employee(
last_name='Все', first_name='Отделы', username='view.all.docs',
department=self.dept, user_location_type='office',
)
it_group = Group.objects.get(name='IT')
viewer.groups.add(it_group)
perm = Permission.objects.get(
codename='view_all_departments', content_type__app_label='reports',
)
viewer.user_permissions.add(perm)
titles = set(visible_report_documents(viewer).values_list('title', flat=True))
self.assertIn(published.title, titles)
self.assertNotIn(draft.title, titles)
+1 -1
View File
@@ -38,7 +38,7 @@ def can_delete_task(user, task) -> bool:
def _employee_dict(emp):
return {
'pk': emp.pk,
'label': emp.get_full_name() or emp.username,
'label': emp.get_full_name() or '—',
'position': emp.post.name if emp.post_id else '',
'photo': emp.photo.url if emp.photo else '',
}
+1
View File
@@ -146,6 +146,7 @@ class EmployeeDetailTests(TestCase):
self.assertNotIn('Личный телефон', html)
self.assertNotIn('Интеграции', html)
self.assertNotIn(self.employee.personal_phone, html)
self.assertNotIn(self.employee.username, html)
def test_manager_chain_and_subordinates_shown(self):
login_2fa(self.client, self.staff)
@@ -46,11 +46,11 @@
</div>
<div style="border-top:1px solid var(--color-border); padding-top:0.875rem; margin-bottom:1rem; font-size:0.8125rem;">
{% if can_view_pii or is_own_profile %}
<div style="display:flex; justify-content:space-between; gap:0.5rem; margin-bottom:0.375rem;">
<span style="color:var(--color-muted-foreground);">{% trans "Логин" %}</span>
<span style="font-weight:500; text-align:right; word-break:break-all;">{{ employee.username }}</span>
</div>
{% if can_view_pii or is_own_profile %}
{% if employee.email %}
<div style="display:flex; justify-content:space-between; gap:0.5rem; margin-bottom:0.375rem;">
<span style="color:var(--color-muted-foreground);">Email</span>