b2f0c2a1e2
Ранее разделы с perm=None (Новости, Задачи, Дислокация, Документы, Чат, Календарь, HR-портал, Справка) были доступны любому вошедшему — сотрудник без назначенной группы видел почти весь портал. Теперь такие разделы требуют хотя бы одной группы; без групп доступны только базовые: Главная, Профиль, Поддержка IT. - user_can_access: BASELINE_OPEN_KEYS + gate «есть ли группа» для perm=None - PortalSectionAccessMiddleware: барьер по префиксу URL (в т.ч. вложенные страницы вроде /tasks/5/), /settings/ не гейтим (редирект на свой профиль) - index.html: пункты меню под can_section (сайдбар, дропдаун, bottom-nav), секция «Управление» скрывается целиком, если пусто - тесты: политика без-группы + прямой заход по URL; тест-фабрики выдают группу (реальный сотрудник её имеет) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
249 lines
9.5 KiB
Python
249 lines
9.5 KiB
Python
"""Тесты портального календаря."""
|
|
from datetime import date, time, timedelta
|
|
|
|
from django.contrib.auth.models import Group, Permission
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.test import TestCase
|
|
|
|
from apps.employees.models import EmployeeHrRequest
|
|
from apps.visit_manager.calendar_service import build_calendar_context
|
|
from apps.visit_manager.models import (
|
|
CalendarEvent, Holiday, SchedulePattern, ScheduleTemplate, Shift, WorkSchedule,
|
|
)
|
|
from core.models.portal_access import PortalAccess
|
|
from core.test_utils import login_2fa
|
|
from core.tests_portal_access import make_employee
|
|
|
|
|
|
def _hr_perm():
|
|
ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False)
|
|
return Permission.objects.get(content_type=ct, codename='access_hr_portal')
|
|
|
|
|
|
def _events_on(ctx, day: date, kind: str | None = None):
|
|
for week in ctx['cal_weeks']:
|
|
for cell in week:
|
|
if cell and cell['date'] == day:
|
|
events = cell['events']
|
|
if kind is None:
|
|
return events
|
|
return [e for e in events if e['kind'] == kind]
|
|
return []
|
|
|
|
|
|
class CalendarServiceTests(TestCase):
|
|
def setUp(self):
|
|
self.user = make_employee(
|
|
username='cal.me',
|
|
first_name='Иван',
|
|
last_name='Петров',
|
|
birth_date=date(1990, 9, 15),
|
|
)
|
|
self.colleague = make_employee(
|
|
username='cal.other',
|
|
first_name='Оля',
|
|
last_name='Сидорова',
|
|
birth_date=date(1992, 9, 20),
|
|
)
|
|
self.hr = make_employee(
|
|
username='cal.hr',
|
|
first_name='HR',
|
|
last_name='Менеджер',
|
|
is_staff=True,
|
|
totp_enabled=True,
|
|
)
|
|
self.hr.user_permissions.add(_hr_perm())
|
|
|
|
def test_month_grid_builds(self):
|
|
ctx = build_calendar_context(self.user, month_str='2026-09', scope='mine')
|
|
self.assertEqual(ctx['cal_month'], 9)
|
|
self.assertEqual(ctx['cal_year'], 2026)
|
|
self.assertTrue(ctx['cal_weeks'])
|
|
days = [
|
|
cell['day']
|
|
for week in ctx['cal_weeks']
|
|
for cell in week
|
|
if cell
|
|
]
|
|
self.assertEqual(days, list(range(1, 31)))
|
|
|
|
def test_holiday_appears(self):
|
|
Holiday.objects.create(
|
|
name='Тест-свято', date=date(2026, 9, 10), is_working_day=False,
|
|
)
|
|
ctx = build_calendar_context(self.user, month_str='2026-09', scope='mine')
|
|
holidays = _events_on(ctx, date(2026, 9, 10), 'holiday')
|
|
self.assertTrue(any(e['label'] == 'Тест-свято' for e in holidays))
|
|
|
|
def test_own_leave_visible_in_mine(self):
|
|
EmployeeHrRequest.objects.create(
|
|
employee=self.user,
|
|
request_type='vacation',
|
|
date_from=date(2026, 9, 8),
|
|
date_to=date(2026, 9, 10),
|
|
status='approved',
|
|
)
|
|
EmployeeHrRequest.objects.create(
|
|
employee=self.colleague,
|
|
request_type='vacation',
|
|
date_from=date(2026, 9, 8),
|
|
date_to=date(2026, 9, 10),
|
|
status='approved',
|
|
)
|
|
ctx = build_calendar_context(self.user, month_str='2026-09', scope='mine')
|
|
leaves = _events_on(ctx, date(2026, 9, 9), 'leave')
|
|
self.assertEqual(len(leaves), 1)
|
|
self.assertIn('Отпуск', leaves[0]['label'])
|
|
|
|
def test_colleague_leave_visible_in_team_for_hr(self):
|
|
EmployeeHrRequest.objects.create(
|
|
employee=self.colleague,
|
|
request_type='vacation',
|
|
date_from=date(2026, 9, 12),
|
|
date_to=date(2026, 9, 12),
|
|
status='approved',
|
|
)
|
|
mine = build_calendar_context(self.user, month_str='2026-09', scope='mine')
|
|
self.assertEqual(_events_on(mine, date(2026, 9, 12), 'leave'), [])
|
|
|
|
team_hr = build_calendar_context(self.hr, month_str='2026-09', scope='team')
|
|
leaves = _events_on(team_hr, date(2026, 9, 12), 'leave')
|
|
self.assertTrue(leaves)
|
|
|
|
def test_birthday_in_month(self):
|
|
ctx = build_calendar_context(self.user, month_str='2026-09', scope='mine')
|
|
bdays = _events_on(ctx, date(2026, 9, 15), 'birthday')
|
|
self.assertEqual(len(bdays), 1)
|
|
|
|
team = build_calendar_context(self.hr, month_str='2026-09', scope='team')
|
|
team_bdays = _events_on(team, date(2026, 9, 20), 'birthday')
|
|
self.assertTrue(team_bdays)
|
|
|
|
def test_shift_chip_for_work_schedule(self):
|
|
from core.models import Directory, DirectoryType
|
|
from apps.locations.models import Pharmacy
|
|
|
|
org_type, _ = DirectoryType.objects.get_or_create(
|
|
code='organization', defaults={'name': 'Организация'},
|
|
)
|
|
region_type, _ = DirectoryType.objects.get_or_create(
|
|
code='region', defaults={'name': 'Регион'},
|
|
)
|
|
region, _ = Directory.objects.get_or_create(
|
|
directory_type=region_type, code='cal', defaults={'name': 'Кал'},
|
|
)
|
|
org, _ = Directory.objects.get_or_create(
|
|
directory_type=org_type, code='calorg', defaults={'name': 'КалОрг'},
|
|
)
|
|
pharmacy = Pharmacy.objects.create(
|
|
organization=org, location_region=region,
|
|
pharmacy_number=501, location_type='pharmacy',
|
|
street='Test', house_number='1',
|
|
)
|
|
self.user.user_pharmacy = pharmacy
|
|
self.user.user_location_type = 'pharmacy'
|
|
self.user.save(update_fields=['user_pharmacy', 'user_location_type'])
|
|
|
|
day_shift = Shift.objects.create(
|
|
name='Денна', shift_type='day',
|
|
start_time=time(9, 0), end_time=time(18, 0),
|
|
is_day_off=False,
|
|
)
|
|
template = ScheduleTemplate.objects.create(
|
|
name='5/2', template_type='fixed',
|
|
)
|
|
for weekday in range(5):
|
|
SchedulePattern.objects.create(
|
|
schedule_template=template, weekday=weekday, shift=day_shift,
|
|
)
|
|
WorkSchedule.objects.create(
|
|
employee=self.user,
|
|
schedule_template=template,
|
|
location=pharmacy,
|
|
start_date=date(2026, 9, 1),
|
|
is_active=True,
|
|
is_primary=True,
|
|
)
|
|
# 2026-09-07 is Monday
|
|
ctx = build_calendar_context(self.user, month_str='2026-09', scope='mine')
|
|
shifts = _events_on(ctx, date(2026, 9, 7), 'shift')
|
|
self.assertTrue(shifts)
|
|
self.assertIn('Денна', shifts[0]['label'])
|
|
|
|
|
|
class CalendarEventViewsTests(TestCase):
|
|
def setUp(self):
|
|
self.user = make_employee(username='cal.author', totp_enabled=False)
|
|
self.stranger = make_employee(username='cal.stranger', totp_enabled=False)
|
|
self.staff = make_employee(
|
|
username='cal.staff', is_staff=True, totp_enabled=True,
|
|
)
|
|
# Любая группа = доступ к открытым разделам (в т.ч. /calendar/).
|
|
grp = Group.objects.get_or_create(name='Сотрудники (тест)')[0]
|
|
for u in (self.user, self.stranger, self.staff):
|
|
u.groups.add(grp)
|
|
|
|
def test_create_event(self):
|
|
login_2fa(self.client, self.user)
|
|
resp = self.client.post(
|
|
'/calendar/events/',
|
|
{
|
|
'title': 'Співбесіда Аня',
|
|
'event_type': 'interview',
|
|
'visibility': 'company',
|
|
'date_from': '2026-09-14',
|
|
'date_to': '2026-09-14',
|
|
'time_from': '10:00',
|
|
'month': '2026-09',
|
|
'scope': 'mine',
|
|
},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertTrue(CalendarEvent.objects.filter(title='Співбесіда Аня').exists())
|
|
self.assertContains(resp, 'Событие создано')
|
|
self.assertContains(resp, 'Співбесіда Аня')
|
|
|
|
def test_delete_forbidden_for_stranger(self):
|
|
event = CalendarEvent.objects.create(
|
|
title='Private',
|
|
event_type='other',
|
|
date_from=date(2026, 9, 5),
|
|
date_to=date(2026, 9, 5),
|
|
visibility='company',
|
|
created_by=self.user,
|
|
)
|
|
login_2fa(self.client, self.stranger)
|
|
resp = self.client.post(
|
|
f'/calendar/events/{event.pk}/delete/',
|
|
{'month': '2026-09', 'scope': 'mine'},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 403)
|
|
self.assertTrue(CalendarEvent.objects.filter(pk=event.pk).exists())
|
|
|
|
def test_delete_by_author(self):
|
|
event = CalendarEvent.objects.create(
|
|
title='To delete',
|
|
event_type='meeting',
|
|
date_from=date(2026, 9, 5),
|
|
date_to=date(2026, 9, 5),
|
|
visibility='personal',
|
|
created_by=self.user,
|
|
)
|
|
login_2fa(self.client, self.user)
|
|
resp = self.client.post(
|
|
f'/calendar/events/{event.pk}/delete/',
|
|
{'month': '2026-09', 'scope': 'mine'},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertFalse(CalendarEvent.objects.filter(pk=event.pk).exists())
|
|
|
|
def test_calendar_page_renders(self):
|
|
login_2fa(self.client, self.user)
|
|
resp = self.client.get('/calendar/?month=2026-09&scope=mine', HTTP_HX_REQUEST='true')
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, 'Вересень 2026')
|
|
self.assertContains(resp, 'Новое событие')
|