diff --git a/core/dashboard.py b/core/dashboard.py index 5d6b91d..213a084 100644 --- a/core/dashboard.py +++ b/core/dashboard.py @@ -15,8 +15,9 @@ class DashboardWidget: key: str label: str description: str - template_name: str # pages/dashboard_widgets/.html - context_builder: Callable | None = None # (request) -> dict | None + template_name: str # pages/dashboard_widgets/.html + context_builder: Callable | None = None # (request) -> dict | None + required_section: str | None = None # ключ раздела портала для доступа к виджету @property def template(self) -> str: @@ -113,7 +114,7 @@ DASHBOARD_WIDGETS: tuple[DashboardWidget, ...] = ( DashboardWidget( 'metrics', 'Метрики', 'Сводка: сотрудники онлайн, аптеки, открытые IT-заявки, справочники.', - 'metrics', _ctx_metrics, + 'metrics', _ctx_metrics, required_section='reports', ), DashboardWidget( 'my_tasks', 'Мои задачи', @@ -164,6 +165,19 @@ def valid_keys(keys) -> list: return result +def widget_allowed(widget: DashboardWidget, user) -> bool: + """Доступен ли виджет пользователю (по разделу портала).""" + if widget.required_section is None: + return True + from core.portal_access import user_can_access + return user_can_access(user, widget.required_section) + + +def accessible_keys(user) -> list: + """Ключи виджетов, доступных пользователю (в порядке реестра).""" + return [w.key for w in DASHBOARD_WIDGETS if widget_allowed(w, user)] + + def default_keys_for(location_type: str | None) -> list: from core.models import DashboardDefaults keys = valid_keys(DashboardDefaults.load().keys_for(location_type or '')) @@ -171,15 +185,21 @@ def default_keys_for(location_type: str | None) -> list: def resolve_layout_keys(user) -> list: - """Ключи виджетов для сотрудника: своя раскладка или дефолт по локации.""" + """Ключи виджетов для сотрудника: своя раскладка или дефолт по локации. + + Всегда отфильтровано по доступности — недоступный виджет не покажется, + даже если остался в сохранённой раскладке или в наборе по умолчанию. + """ from core.models import DashboardLayout + allowed = set(accessible_keys(user)) layout = DashboardLayout.objects.filter(employee=user).first() if layout is not None and layout.widgets: - keys = valid_keys(layout.widgets) + keys = [k for k in valid_keys(layout.widgets) if k in allowed] if keys: return keys - return default_keys_for(getattr(user, 'user_location_type', None)) + default = default_keys_for(getattr(user, 'user_location_type', None)) + return [k for k in default if k in allowed] def build_dashboard_context(request) -> dict: @@ -209,22 +229,28 @@ def _has_custom_layout(user) -> bool: def customize_rows(user) -> list: - """Строки для страницы настройки: включённые (в порядке) + выключенные.""" + """Строки для страницы настройки: включённые (в порядке) + выключенные. + + Только доступные пользователю виджеты — нельзя включить то, к чему нет + доступа. + """ + allowed = set(accessible_keys(user)) enabled = resolve_layout_keys(user) enabled_set = set(enabled) rows = [{'widget': WIDGETS_BY_KEY[k], 'enabled': True} for k in enabled] for widget in DASHBOARD_WIDGETS: - if widget.key not in enabled_set: + if widget.key in allowed and widget.key not in enabled_set: rows.append({'widget': widget, 'enabled': False}) return rows def save_layout(user, ordered_keys, enabled_keys) -> list: - """Сохранить раскладку: включённые ключи в заданном порядке.""" + """Сохранить раскладку: включённые и доступные ключи в заданном порядке.""" from core.models import DashboardLayout + allowed = set(accessible_keys(user)) enabled_set = set(enabled_keys) - result = [k for k in valid_keys(ordered_keys) if k in enabled_set] + result = [k for k in valid_keys(ordered_keys) if k in enabled_set and k in allowed] DashboardLayout.objects.update_or_create( employee=user, defaults={'widgets': result}, ) diff --git a/core/tests_dashboard.py b/core/tests_dashboard.py index 9261f57..9ff7219 100644 --- a/core/tests_dashboard.py +++ b/core/tests_dashboard.py @@ -27,20 +27,30 @@ def make_employee(**overrides): return emp +def grant_reports(user): + """Выдать доступ к разделу 'reports' (нужен виджету 'metrics').""" + from django.contrib.auth.models import Group + from core.tests_portal_access import portal_perm + group = Group.objects.create(name=f'reports-{user.pk}') + group.permissions.add(portal_perm('access_reports')) + user.groups.add(group) + return user + + class DashboardLayoutLogicTests(TestCase): - def test_default_fallback_all_widgets(self): + def test_default_fallback_all_accessible(self): user = make_employee() - self.assertEqual(dash.resolve_layout_keys(user), dash.DEFAULT_WIDGET_KEYS) + self.assertEqual(dash.resolve_layout_keys(user), dash.accessible_keys(user)) def test_default_per_location_type(self): d = DashboardDefaults.load() - d.office = ['metrics', 'news'] - d.pharmacy = ['my_tasks'] + d.office = ['news', 'my_tasks'] + d.pharmacy = ['upcoming'] d.save() office = make_employee(user_location_type='office') pharmacy = make_employee(user_location_type='pharmacy') - self.assertEqual(dash.resolve_layout_keys(office), ['metrics', 'news']) - self.assertEqual(dash.resolve_layout_keys(pharmacy), ['my_tasks']) + self.assertEqual(dash.resolve_layout_keys(office), ['news', 'my_tasks']) + self.assertEqual(dash.resolve_layout_keys(pharmacy), ['upcoming']) def test_personal_layout_overrides_default(self): d = DashboardDefaults.load() @@ -58,7 +68,7 @@ class DashboardLayoutLogicTests(TestCase): ) def test_save_layout_filters_disabled(self): - user = make_employee() + user = grant_reports(make_employee()) saved = dash.save_layout(user, ['welcome', 'news', 'metrics'], ['welcome', 'metrics']) self.assertEqual(saved, ['welcome', 'metrics']) self.assertEqual(DashboardLayout.objects.get(employee=user).widgets, ['welcome', 'metrics']) @@ -69,20 +79,49 @@ class DashboardLayoutLogicTests(TestCase): self.assertTrue(DashboardLayout.objects.filter(employee=user).exists()) dash.reset_layout(user) self.assertFalse(DashboardLayout.objects.filter(employee=user).exists()) - self.assertEqual(dash.resolve_layout_keys(user), dash.DEFAULT_WIDGET_KEYS) + self.assertEqual(dash.resolve_layout_keys(user), dash.accessible_keys(user)) def test_customize_rows_enabled_first(self): - user = make_employee() + user = grant_reports(make_employee()) dash.save_layout(user, ['news', 'metrics'], ['news', 'metrics']) rows = dash.customize_rows(user) keys = [r['widget'].key for r in rows] - # включённые первыми в своём порядке, затем остальные + # включённые первыми в своём порядке, затем остальные доступные self.assertEqual(keys[:2], ['news', 'metrics']) - self.assertEqual(set(keys), set(dash.DEFAULT_WIDGET_KEYS)) + self.assertEqual(set(keys), set(dash.accessible_keys(user))) self.assertTrue(rows[0]['enabled']) self.assertFalse(rows[2]['enabled']) +class DashboardWidgetAccessTests(TestCase): + def test_metrics_hidden_without_reports(self): + user = make_employee() + self.assertNotIn('metrics', dash.accessible_keys(user)) + self.assertNotIn('metrics', dash.resolve_layout_keys(user)) + keys = [r['widget'].key for r in dash.customize_rows(user)] + self.assertNotIn('metrics', keys) + + def test_metrics_visible_with_reports(self): + user = grant_reports(make_employee()) + self.assertIn('metrics', dash.accessible_keys(user)) + self.assertIn('metrics', dash.resolve_layout_keys(user)) + + def test_save_layout_drops_inaccessible(self): + user = make_employee() # без доступа к reports + saved = dash.save_layout(user, ['metrics', 'news'], ['metrics', 'news']) + self.assertEqual(saved, ['news']) # metrics отброшен + + def test_saved_inaccessible_widget_not_rendered(self): + """Виджет, оставшийся в раскладке, но недоступный — не отдаётся.""" + user = grant_reports(make_employee()) + dash.save_layout(user, ['metrics', 'news'], ['metrics', 'news']) + # снимаем доступ: убираем группу reports + user.groups.filter(name__startswith='reports-').delete() + user = Employee.objects.get(pk=user.pk) # сброс кэша прав + self.assertNotIn('metrics', dash.resolve_layout_keys(user)) + self.assertIn('news', dash.resolve_layout_keys(user)) + + class DashboardWidgetRegistryTests(TestCase): def test_new_widgets_registered(self): for key in ('upcoming', 'my_hr_requests', 'my_tickets'): @@ -127,7 +166,7 @@ class DashboardViewTests(TestCase): self.assertContains(resp, 'Настройка дашборда') def test_customize_post_saves(self): - user = make_employee() + user = grant_reports(make_employee()) # reports нужен для виджета metrics login_2fa(self.client, user) resp = self.client.post( '/dashboard/customize/', diff --git a/templates/pages/dashboard_widgets/quick_access.html b/templates/pages/dashboard_widgets/quick_access.html index 277280d..35458ae 100644 --- a/templates/pages/dashboard_widgets/quick_access.html +++ b/templates/pages/dashboard_widgets/quick_access.html @@ -18,6 +18,7 @@ {% endif %} + {% if request.user|can_section:'documents' %}
@@ -27,7 +28,9 @@
{% trans "Документы" %}
+ {% endif %} + {% if request.user|can_section:'tasks' %}
@@ -37,7 +40,9 @@
{% trans "Задачи" %}
+ {% endif %} + {% if request.user|can_section:'calendar' %}
@@ -46,6 +51,7 @@
{% trans "Календарь" %}
+ {% endif %} {% if request.user|can_section:'databases' %} @@ -58,6 +64,7 @@ {% endif %} + {% if request.user|can_section:'hr_portal' %}
@@ -68,6 +75,7 @@
{% trans "Сотрудники" %}
+ {% endif %}