1443 lines
56 KiB
Python
1443 lines
56 KiB
Python
from django.contrib.auth.models import User
|
|
from django.test import TestCase, Client
|
|
from django.urls import reverse
|
|
|
|
from common.models import UserConfig, Thematic, UserThematics, AppView
|
|
|
|
|
|
class SignViewTests(TestCase):
|
|
"""Tests for the sign view functionality."""
|
|
|
|
def setUp(self):
|
|
"""Set up test data."""
|
|
self.client = Client()
|
|
|
|
# Create application views
|
|
self.asset_view = AppView.objects.create(
|
|
code='asset_management',
|
|
name_fr='Gestion des assets',
|
|
name_nl='Asset Management',
|
|
icon='bi-database',
|
|
url_name='home:index',
|
|
is_default=True,
|
|
order=1
|
|
)
|
|
self.sign_view = AppView.objects.create(
|
|
code='sign',
|
|
name_fr='Signalisation',
|
|
name_nl='Signalisatie',
|
|
icon='bi-signpost',
|
|
url_name='sign:index',
|
|
order=2
|
|
)
|
|
|
|
# Create a thematic for sign
|
|
self.sign_thematic = Thematic.objects.create(
|
|
code='sign',
|
|
name_fr='Signalisation',
|
|
name_nl='Signalisatie',
|
|
icon='bi-signpost',
|
|
color='#0022ff'
|
|
)
|
|
|
|
# Create a user with access to sign view
|
|
self.user_with_access = User.objects.create_user(
|
|
username='signuser',
|
|
password='testpass123'
|
|
)
|
|
self.config_with_access = UserConfig.objects.create(
|
|
user=self.user_with_access
|
|
)
|
|
self.config_with_access.accessible_views.add(self.asset_view, self.sign_view)
|
|
|
|
# Give the user access to sign thematic
|
|
UserThematics.objects.create(
|
|
user_config=self.config_with_access,
|
|
thematic=self.sign_thematic,
|
|
can_view_assets=True,
|
|
can_edit_assets=False
|
|
)
|
|
|
|
# Create a user without access to sign view
|
|
self.user_without_access = User.objects.create_user(
|
|
username='notsignuser',
|
|
password='testpass123'
|
|
)
|
|
self.config_without_access = UserConfig.objects.create(
|
|
user=self.user_without_access
|
|
)
|
|
self.config_without_access.accessible_views.add(self.asset_view)
|
|
|
|
# Create a user with only sign access
|
|
self.user_sign_only = User.objects.create_user(
|
|
username='signonly',
|
|
password='testpass123'
|
|
)
|
|
self.config_sign_only = UserConfig.objects.create(
|
|
user=self.user_sign_only
|
|
)
|
|
self.config_sign_only.accessible_views.add(self.sign_view)
|
|
UserThematics.objects.create(
|
|
user_config=self.config_sign_only,
|
|
thematic=self.sign_thematic,
|
|
can_view_assets=True
|
|
)
|
|
|
|
def test_sign_index_access_granted(self):
|
|
"""Test that users with sign access can view the page."""
|
|
self.client.force_login(self.user_with_access)
|
|
response = self.client.get(reverse('sign:index'))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTemplateUsed(response, 'sign/index.html')
|
|
|
|
def test_sign_index_access_denied(self):
|
|
"""Test that users without sign access get 403 Forbidden."""
|
|
self.client.force_login(self.user_without_access)
|
|
response = self.client.get(reverse('sign:index'))
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_sign_requires_authentication(self):
|
|
"""Test that unauthenticated users are redirected to login."""
|
|
response = self.client.get(reverse('sign:index'))
|
|
self.assertRedirects(
|
|
response,
|
|
f"{reverse('login')}?next={reverse('sign:index')}"
|
|
)
|
|
|
|
def test_sign_index_contains_required_elements(self):
|
|
"""Test that the sign page contains required UI elements."""
|
|
self.client.force_login(self.user_with_access)
|
|
response = self.client.get(reverse('sign:index'))
|
|
|
|
# Check for main container
|
|
self.assertContains(response, 'sign-container')
|
|
|
|
# Check for left panel
|
|
self.assertContains(response, 'sign-panel')
|
|
|
|
# Check for map container
|
|
self.assertContains(response, 'sign-map')
|
|
|
|
# Check for tabs
|
|
self.assertContains(response, 'tasks-tab')
|
|
self.assertContains(response, 'filters-tab')
|
|
self.assertContains(response, 'regulations-tab')
|
|
|
|
def test_sign_filter_parameters(self):
|
|
"""Test that filter parameters are passed to context."""
|
|
self.client.force_login(self.user_with_access)
|
|
response = self.client.get(
|
|
reverse('sign:index'),
|
|
{'municipality': ['1', '2'], 'panel': 'filters'}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.context['active_panel'], 'filters')
|
|
self.assertIn('1', response.context['selected_municipalities'])
|
|
self.assertIn('2', response.context['selected_municipalities'])
|
|
|
|
|
|
class ViewSwitcherTests(TestCase):
|
|
"""Tests for the view switcher functionality in the header."""
|
|
|
|
def setUp(self):
|
|
"""Set up test data."""
|
|
self.client = Client()
|
|
|
|
# Create application views
|
|
self.asset_view = AppView.objects.create(
|
|
code='asset_management',
|
|
name_fr='Gestion des assets',
|
|
name_nl='Asset Management',
|
|
icon='bi-database',
|
|
url_name='home:index',
|
|
is_default=True,
|
|
order=1
|
|
)
|
|
self.sign_view = AppView.objects.create(
|
|
code='sign',
|
|
name_fr='Signalisation',
|
|
name_nl='Signalisatie',
|
|
icon='bi-signpost',
|
|
url_name='sign:index',
|
|
order=2
|
|
)
|
|
|
|
# Create users with different access levels
|
|
self.user_both_views = User.objects.create_user(
|
|
username='bothviews',
|
|
password='testpass123'
|
|
)
|
|
self.config_both = UserConfig.objects.create(
|
|
user=self.user_both_views
|
|
)
|
|
self.config_both.accessible_views.add(self.asset_view, self.sign_view)
|
|
|
|
self.user_asset_only = User.objects.create_user(
|
|
username='assetonly',
|
|
password='testpass123'
|
|
)
|
|
self.config_asset = UserConfig.objects.create(
|
|
user=self.user_asset_only
|
|
)
|
|
self.config_asset.accessible_views.add(self.asset_view)
|
|
|
|
self.user_sign_only = User.objects.create_user(
|
|
username='signonly',
|
|
password='testpass123'
|
|
)
|
|
self.config_sign = UserConfig.objects.create(
|
|
user=self.user_sign_only
|
|
)
|
|
self.config_sign.accessible_views.add(self.sign_view)
|
|
|
|
def test_view_switcher_shown_for_both_views(self):
|
|
"""Test that view switcher is shown for users with access to both views."""
|
|
self.client.force_login(self.user_both_views)
|
|
response = self.client.get(reverse('home:index'))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, 'viewSwitcherDropdown')
|
|
|
|
def test_view_switcher_hidden_for_single_view_asset(self):
|
|
"""Test that view switcher dropdown is not shown for asset-only users."""
|
|
self.client.force_login(self.user_asset_only)
|
|
response = self.client.get(reverse('home:index'))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
# The dropdown should not be shown
|
|
self.assertNotContains(response, 'viewSwitcherDropdown')
|
|
|
|
|
|
class UserConfigViewAccessTests(TestCase):
|
|
"""Tests for UserConfig view access with generic AppView model."""
|
|
|
|
def setUp(self):
|
|
"""Set up test data."""
|
|
self.asset_view = AppView.objects.create(
|
|
code='asset_management',
|
|
name_fr='Gestion des assets',
|
|
name_nl='Asset Management',
|
|
icon='bi-database',
|
|
url_name='home:index',
|
|
is_default=True,
|
|
order=1
|
|
)
|
|
self.sign_view = AppView.objects.create(
|
|
code='sign',
|
|
name_fr='Signalisation',
|
|
name_nl='Signalisatie',
|
|
icon='bi-signpost',
|
|
url_name='sign:index',
|
|
order=2
|
|
)
|
|
|
|
def test_can_access_view_method(self):
|
|
"""Test the can_access_view method."""
|
|
user = User.objects.create_user(username='testuser', password='pass')
|
|
config = UserConfig.objects.create(user=user)
|
|
config.accessible_views.add(self.sign_view)
|
|
|
|
self.assertTrue(config.can_access_view('sign'))
|
|
self.assertFalse(config.can_access_view('asset_management'))
|
|
|
|
def test_can_add_multiple_views(self):
|
|
"""Test that multiple views can be added."""
|
|
user = User.objects.create_user(username='testuser', password='pass')
|
|
config = UserConfig.objects.create(user=user)
|
|
config.accessible_views.add(self.asset_view, self.sign_view)
|
|
|
|
self.assertTrue(config.can_access_view('asset_management'))
|
|
self.assertTrue(config.can_access_view('sign'))
|
|
self.assertEqual(config.accessible_views.count(), 2)
|
|
|
|
def test_get_accessible_views_ordered(self):
|
|
"""Test that get_accessible_views returns views in order."""
|
|
user = User.objects.create_user(username='testuser', password='pass')
|
|
config = UserConfig.objects.create(user=user)
|
|
config.accessible_views.add(self.sign_view, self.asset_view)
|
|
|
|
views = list(config.get_accessible_views())
|
|
self.assertEqual(views[0].code, 'asset_management') # order=1
|
|
self.assertEqual(views[1].code, 'sign') # order=2
|
|
|
|
|
|
class AppViewModelTests(TestCase):
|
|
"""Tests for the AppView model."""
|
|
|
|
def test_app_view_creation(self):
|
|
"""Test creating an AppView."""
|
|
view = AppView.objects.create(
|
|
code='test_view',
|
|
name_fr='Vue de test',
|
|
name_nl='Test view',
|
|
icon='bi-test',
|
|
url_name='test:index',
|
|
is_default=False,
|
|
order=10
|
|
)
|
|
|
|
self.assertEqual(view.code, 'test_view')
|
|
self.assertEqual(view.name_fr, 'Vue de test')
|
|
self.assertEqual(view.url_name, 'test:index')
|
|
|
|
def test_app_view_name_property(self):
|
|
"""Test the name property returns the correct language."""
|
|
view = AppView.objects.create(
|
|
code='test_view',
|
|
name_fr='Vue de test',
|
|
name_nl='Test view',
|
|
icon='bi-test',
|
|
url_name='test:index'
|
|
)
|
|
|
|
# Default language should be French
|
|
self.assertEqual(view.name, 'Vue de test')
|
|
|
|
|
|
class ESignPermissionTests(TestCase):
|
|
"""Tests for the refined eSign permissions."""
|
|
|
|
def setUp(self):
|
|
from django.contrib.auth.models import Permission
|
|
from common.models import Role
|
|
|
|
# Create basic roles
|
|
self.admin_role, _ = Role.objects.get_or_create(name='admin')
|
|
self.manager_role, _ = Role.objects.get_or_create(name='manager')
|
|
|
|
# Create superuser
|
|
self.superuser = User.objects.create_superuser(username='super', password='password')
|
|
|
|
# Create normal users
|
|
self.user_none = User.objects.create_user(username='none', password='password')
|
|
self.config_none = UserConfig.objects.create(user=self.user_none)
|
|
|
|
self.user_edit = User.objects.create_user(username='edit', password='password')
|
|
self.config_edit = UserConfig.objects.create(user=self.user_edit, esign_can_edit=True)
|
|
|
|
self.user_manage = User.objects.create_user(username='manage', password='password')
|
|
self.config_manage = UserConfig.objects.create(user=self.user_manage, esign_can_manage_rcp=True)
|
|
|
|
self.user_validate = User.objects.create_user(username='validate', password='password')
|
|
self.config_validate = UserConfig.objects.create(user=self.user_validate, esign_can_validate_rcp=True)
|
|
|
|
# Users with roles
|
|
self.user_admin = User.objects.create_user(username='admin_user', password='password')
|
|
self.config_admin = UserConfig.objects.create(user=self.user_admin)
|
|
self.config_admin.roles.add(self.admin_role)
|
|
|
|
self.user_manager = User.objects.create_user(username='manager_user', password='password')
|
|
self.config_manager = UserConfig.objects.create(user=self.user_manager)
|
|
self.config_manager.roles.add(self.manager_role)
|
|
|
|
# User with Django built-in permission
|
|
self.user_django_perm = User.objects.create_user(username='django_perm', password='password')
|
|
self.config_django_perm = UserConfig.objects.create(user=self.user_django_perm)
|
|
add_perm = Permission.objects.filter(codename='add_signpole', content_type__app_label='sign').first()
|
|
if not add_perm:
|
|
add_perm = Permission.objects.filter(codename='add_signpole').first()
|
|
self.user_django_perm.user_permissions.add(add_perm)
|
|
|
|
def test_user_can_edit_esign_assets(self):
|
|
from sign.permissions import user_can_edit_esign_assets
|
|
|
|
# Superuser should be able to edit
|
|
self.assertTrue(user_can_edit_esign_assets(self.superuser))
|
|
|
|
# Normal user should not be able to edit
|
|
self.assertFalse(user_can_edit_esign_assets(self.user_none))
|
|
|
|
# User with esign_can_edit should be able to edit
|
|
self.assertTrue(user_can_edit_esign_assets(self.user_edit))
|
|
|
|
# User with django perm should be able to edit
|
|
# Refresh user instance to load permissions
|
|
user_django = User.objects.get(pk=self.user_django_perm.pk)
|
|
self.assertTrue(user_can_edit_esign_assets(user_django))
|
|
|
|
def test_user_can_manage_regulations(self):
|
|
from sign.permissions import user_can_manage_regulations
|
|
|
|
# Superuser should be able to manage
|
|
self.assertTrue(user_can_manage_regulations(self.superuser))
|
|
|
|
# Normal user should not be able to manage
|
|
self.assertFalse(user_can_manage_regulations(self.user_none))
|
|
|
|
# User with esign_can_manage_rcp should be able to manage
|
|
self.assertTrue(user_can_manage_regulations(self.user_manage))
|
|
|
|
# Admin and manager roles should be able to manage
|
|
self.assertTrue(user_can_manage_regulations(self.user_admin))
|
|
self.assertTrue(user_can_manage_regulations(self.user_manager))
|
|
|
|
def test_user_can_approve_regulation(self):
|
|
from sign.permissions import user_can_approve_regulation
|
|
|
|
# Superuser should be able to approve
|
|
self.assertTrue(user_can_approve_regulation(self.superuser))
|
|
|
|
# Normal user should not be able to approve
|
|
self.assertFalse(user_can_approve_regulation(self.user_none))
|
|
|
|
# User with esign_can_validate_rcp should be able to approve
|
|
self.assertTrue(user_can_approve_regulation(self.user_validate))
|
|
|
|
# Admin role should be able to approve
|
|
self.assertTrue(user_can_approve_regulation(self.user_admin))
|
|
|
|
# Manager role should NOT be able to approve (requires admin or validate permission)
|
|
self.assertFalse(user_can_approve_regulation(self.user_manager))
|
|
|
|
|
|
class ESignOrganizationRestrictionsTests(TestCase):
|
|
"""Tests for eSign Organization restrictions and validation."""
|
|
|
|
def setUp(self):
|
|
from sign.models import Organization, ComplementaryPoliceRegulation
|
|
self.client = Client()
|
|
|
|
# Create basic app views so we can access 'sign' view
|
|
self.sign_view = AppView.objects.create(
|
|
code='sign',
|
|
name_fr='Signalisation',
|
|
name_nl='Signalisatie',
|
|
icon='bi-signpost',
|
|
url_name='sign:index',
|
|
order=2
|
|
)
|
|
|
|
# Create Organizations
|
|
self.org_a = Organization.objects.create(
|
|
name_fr="Commune A (FR)",
|
|
name_nl="Commune A (NL)",
|
|
code="ORG_A",
|
|
organization_type="municipality"
|
|
)
|
|
self.org_b = Organization.objects.create(
|
|
name_fr="Commune B (FR)",
|
|
name_nl="Commune B (NL)",
|
|
code="ORG_B",
|
|
organization_type="municipality"
|
|
)
|
|
|
|
# Create users
|
|
# 1. Superuser
|
|
self.superuser = User.objects.create_superuser(
|
|
username='super_org',
|
|
password='testpass123'
|
|
)
|
|
self.config_super = UserConfig.objects.create(user=self.superuser)
|
|
self.config_super.accessible_views.add(self.sign_view)
|
|
|
|
# 2. Restricted User for Org A (can manage RCP)
|
|
self.user_a = User.objects.create_user(
|
|
username='user_a',
|
|
password='testpass123'
|
|
)
|
|
self.config_a = UserConfig.objects.create(
|
|
user=self.user_a,
|
|
esign_can_manage_rcp=True
|
|
)
|
|
self.config_a.accessible_views.add(self.sign_view)
|
|
self.config_a.esign_organizations.add(self.org_a)
|
|
|
|
# 3. Restricted User for Org B (can manage RCP)
|
|
self.user_b = User.objects.create_user(
|
|
username='user_b',
|
|
password='testpass123'
|
|
)
|
|
self.config_b = UserConfig.objects.create(
|
|
user=self.user_b,
|
|
esign_can_manage_rcp=True
|
|
)
|
|
self.config_b.accessible_views.add(self.sign_view)
|
|
self.config_b.esign_organizations.add(self.org_b)
|
|
|
|
# 4. User without any allowed organizations (Visitor / read-only)
|
|
self.user_visitor = User.objects.create_user(
|
|
username='user_visitor',
|
|
password='testpass123'
|
|
)
|
|
self.config_visitor = UserConfig.objects.create(
|
|
user=self.user_visitor
|
|
)
|
|
self.config_visitor.accessible_views.add(self.sign_view)
|
|
|
|
# Create some complementary police regulations (RCPs)
|
|
self.reg_a = ComplementaryPoliceRegulation.objects.create(
|
|
title="Regulation Org A",
|
|
reference="REF-A-001",
|
|
description="Test A",
|
|
organization=self.org_a,
|
|
status="in_preparation",
|
|
uploaded_by=self.user_a
|
|
)
|
|
self.reg_b = ComplementaryPoliceRegulation.objects.create(
|
|
title="Regulation Org B",
|
|
reference="REF-B-001",
|
|
description="Test B",
|
|
organization=self.org_b,
|
|
status="in_preparation",
|
|
uploaded_by=self.user_b
|
|
)
|
|
|
|
def test_asset_forms_organization_required(self):
|
|
"""Test that SignPoleForm and SignPanelForm require an organization."""
|
|
import json
|
|
from sign.forms import SignPoleForm, SignPanelForm
|
|
|
|
# 1. SignPoleForm validation
|
|
pole_data_no_org = {
|
|
'orientation': 90,
|
|
'lat': 50.85,
|
|
'lon': 4.35
|
|
}
|
|
form_pole = SignPoleForm(data=pole_data_no_org)
|
|
self.assertFalse(form_pole.is_valid())
|
|
self.assertIn('organization', form_pole.errors)
|
|
|
|
pole_data_with_org = pole_data_no_org.copy()
|
|
pole_data_with_org['organization'] = self.org_a.id
|
|
form_pole_ok = SignPoleForm(data=pole_data_with_org)
|
|
self.assertTrue(form_pole_ok.is_valid())
|
|
|
|
# 2. SignPanelForm validation
|
|
panel_data_no_org = {
|
|
'orientation': 180,
|
|
'support_type': 'pole',
|
|
'is_temporary': False,
|
|
'lat': 50.85,
|
|
'lon': 4.35
|
|
}
|
|
form_panel = SignPanelForm(data=panel_data_no_org)
|
|
self.assertFalse(form_panel.is_valid())
|
|
self.assertIn('organization', form_panel.errors)
|
|
|
|
panel_data_with_org = panel_data_no_org.copy()
|
|
panel_data_with_org['organization'] = self.org_a.id
|
|
form_panel_ok = SignPanelForm(data=panel_data_with_org)
|
|
self.assertTrue(form_panel_ok.is_valid())
|
|
|
|
def test_universal_asset_visibility(self):
|
|
"""Test that any logged-in user can view assets without being blocked by organization permissions."""
|
|
import json
|
|
self.client.force_login(self.user_visitor)
|
|
response = self.client.get(reverse('sign:index'))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
response_poles = self.client.post(
|
|
reverse('sign:sign_poles_overview_geojson'),
|
|
data=json.dumps({}),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response_poles.status_code, 200)
|
|
|
|
def test_superuser_bypass_restrictions(self):
|
|
"""Test that superusers can see and modify any regulation across all organizations."""
|
|
import json
|
|
self.client.force_login(self.superuser)
|
|
|
|
# 1. Superuser should see both regulations in list
|
|
response = self.client.get(reverse('sign:regulations_list'))
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, self.reg_a.title)
|
|
self.assertContains(response, self.reg_b.title)
|
|
|
|
# 2. Superuser should see both regulations in search
|
|
response_search = self.client.get(reverse('sign:regulations_search'), {'q': ''})
|
|
self.assertEqual(response_search.status_code, 200)
|
|
results = response_search.json().get('results', [])
|
|
self.assertEqual(len(results), 2)
|
|
|
|
# 3. Superuser can access detail of both
|
|
response_detail_a = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_a.id}))
|
|
self.assertEqual(response_detail_a.status_code, 200)
|
|
response_detail_b = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_b.id}))
|
|
self.assertEqual(response_detail_b.status_code, 200)
|
|
|
|
def test_restricted_user_visibility_and_access(self):
|
|
"""Test that a restricted user can only see and access regulations of their organization."""
|
|
import json
|
|
self.client.force_login(self.user_a)
|
|
|
|
# 1. List only contains reg_a
|
|
response_list = self.client.get(reverse('sign:regulations_list'))
|
|
self.assertEqual(response_list.status_code, 200)
|
|
self.assertContains(response_list, self.reg_a.title)
|
|
self.assertNotContains(response_list, self.reg_b.title)
|
|
|
|
# 2. Search only returns reg_a
|
|
response_search = self.client.get(reverse('sign:regulations_search'), {'q': ''})
|
|
self.assertEqual(response_search.status_code, 200)
|
|
results = response_search.json().get('results', [])
|
|
self.assertEqual(len(results), 1)
|
|
self.assertEqual(results[0]['id'], self.reg_a.id)
|
|
|
|
# 3. Detail view for reg_a works, but reg_b raises PermissionDenied (403)
|
|
response_detail_a = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_a.id}))
|
|
self.assertEqual(response_detail_a.status_code, 200)
|
|
|
|
response_detail_b = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_b.id}))
|
|
self.assertEqual(response_detail_b.status_code, 403)
|
|
|
|
# 4. Docx generation for reg_a works (or handles it, but not 403), but reg_b raises 403
|
|
response_docx_b = self.client.get(reverse('sign:regulation_generate_docx', kwargs={'regulation_id': self.reg_b.id}))
|
|
self.assertEqual(response_docx_b.status_code, 403)
|
|
|
|
def test_regulation_creation_organization_check(self):
|
|
"""Test that creating a regulation is limited to the user's allowed organizations."""
|
|
self.client.force_login(self.user_a)
|
|
|
|
# 1. Try creating a regulation for org_b (not allowed)
|
|
response_create_forbidden = self.client.post(
|
|
reverse('sign:regulation_create'),
|
|
{
|
|
'title': 'New Reg Forbidden',
|
|
'reference': 'REF-F-001',
|
|
'description': 'Description',
|
|
'organization': self.org_b.id
|
|
}
|
|
)
|
|
self.assertEqual(response_create_forbidden.status_code, 403)
|
|
|
|
# 2. Try creating a regulation for org_a (allowed)
|
|
response_create_ok = self.client.post(
|
|
reverse('sign:regulation_create'),
|
|
{
|
|
'title': 'New Reg Allowed',
|
|
'reference': 'REF-OK-001',
|
|
'description': 'Description',
|
|
'organization': self.org_a.id
|
|
}
|
|
)
|
|
self.assertEqual(response_create_ok.status_code, 200)
|
|
self.assertTrue(response_create_ok.json().get('success'))
|
|
|
|
def test_regulation_editing_organization_check(self):
|
|
"""Test that editing a regulation and updating its organization checks user permissions."""
|
|
self.client.force_login(self.user_a)
|
|
|
|
# 1. Edit reg_a: change title, keep org_a -> OK
|
|
response_edit_ok = self.client.post(
|
|
reverse('sign:regulation_edit', kwargs={'regulation_id': self.reg_a.id}),
|
|
{
|
|
'title': 'Updated Title',
|
|
'reference': self.reg_a.reference,
|
|
'description': self.reg_a.description,
|
|
'organization': self.org_a.id
|
|
}
|
|
)
|
|
self.assertEqual(response_edit_ok.status_code, 200)
|
|
self.assertTrue(response_edit_ok.json().get('success'))
|
|
|
|
# 2. Edit reg_a: try to change organization to org_b -> Forbidden
|
|
response_edit_forbidden = self.client.post(
|
|
reverse('sign:regulation_edit', kwargs={'regulation_id': self.reg_a.id}),
|
|
{
|
|
'title': 'Updated Title',
|
|
'reference': self.reg_a.reference,
|
|
'description': self.reg_a.description,
|
|
'organization': self.org_b.id
|
|
}
|
|
)
|
|
self.assertEqual(response_edit_forbidden.status_code, 403)
|
|
|
|
# 3. Try to edit reg_b (which belongs to org_b) -> Forbidden (user_can_edit_regulation checks organization)
|
|
response_edit_other_rcp = self.client.post(
|
|
reverse('sign:regulation_edit', kwargs={'regulation_id': self.reg_b.id}),
|
|
{
|
|
'title': 'Updated Title Other',
|
|
'reference': self.reg_b.reference,
|
|
'description': self.reg_b.description,
|
|
'organization': self.org_b.id
|
|
}
|
|
)
|
|
self.assertEqual(response_edit_other_rcp.status_code, 403)
|
|
|
|
|
|
class CopySignDataCommandTests(TestCase):
|
|
"""Tests for the copy_sign_data management command."""
|
|
|
|
def test_copy_sign_data_command(self):
|
|
from io import StringIO
|
|
from django.core.management import call_command
|
|
from django.contrib.gis.geos import Point
|
|
from common.models import Municipality, AdrStreet
|
|
from assets.models import AssetCategory
|
|
from assets.models.sign import (
|
|
SignPanelType as AssetsSignPanelType,
|
|
SignPoleModel as AssetsSignPoleModel,
|
|
SignPanelModel as AssetsSignPanelModel,
|
|
SignStreet as AssetsSignStreet,
|
|
SignPole as AssetsSignPole,
|
|
SignPanel as AssetsSignPanel,
|
|
ComplementaryPoliceRegulation as AssetsComplementaryPoliceRegulation,
|
|
)
|
|
from sign.models import (
|
|
SignPanelType,
|
|
SignPoleModel,
|
|
SignPanelModel,
|
|
SignStreet,
|
|
SignPole,
|
|
SignPanel,
|
|
ComplementaryPoliceRegulation,
|
|
Organization,
|
|
)
|
|
|
|
# 1. Setup Source Data in Assets
|
|
muni = Municipality.objects.create(fid="1000", name_fr="Bruxelles", name_nl="Brussel")
|
|
adr_street = AdrStreet.objects.create(
|
|
fid="STR-1",
|
|
name_fr="Rue Royale",
|
|
name_nl="Koningsstraat",
|
|
municipality=muni,
|
|
)
|
|
org = Organization.objects.create(
|
|
code="SPRB",
|
|
name_fr="Bruxelles Mobilité",
|
|
name_nl="Brussel Mobiliteit",
|
|
organization_type="region",
|
|
)
|
|
|
|
cat = AssetCategory.objects.create(code="sign_cat", name_fr="Sign Cat")
|
|
src_ptype = AssetsSignPanelType.objects.create(
|
|
code="C1",
|
|
name_fr="Sens interdit",
|
|
name_nl="Verboden richting",
|
|
default_width=60.0,
|
|
default_height=60.0,
|
|
)
|
|
src_pmodel = AssetsSignPoleModel.objects.create(
|
|
code="PM-1",
|
|
name_fr="Poteau Acier 3m",
|
|
category=cat,
|
|
color="Gris",
|
|
height=3.0,
|
|
)
|
|
src_panel_model = AssetsSignPanelModel.objects.create(
|
|
code="PANM-1",
|
|
name_fr="Modèle C1 60",
|
|
category=cat,
|
|
shape="rond",
|
|
)
|
|
src_street = AssetsSignStreet.objects.create(
|
|
code="STR-ROYALE",
|
|
name_fr="Rue Royale",
|
|
name_nl="Koningsstraat",
|
|
municipality=muni,
|
|
)
|
|
src_pole = AssetsSignPole.objects.create(
|
|
code="POL-001",
|
|
name_fr="Poteau 1",
|
|
street=src_street,
|
|
model=src_pmodel,
|
|
category=cat,
|
|
geom=Point(149000, 171000, srid=3812),
|
|
)
|
|
src_panel = AssetsSignPanel.objects.create(
|
|
code="PAN-001",
|
|
name_fr="Panneau C1",
|
|
street=src_street,
|
|
pole=src_pole,
|
|
model=src_panel_model,
|
|
signpanel_type=src_ptype,
|
|
category=cat,
|
|
geom=Point(149000, 171000, srid=3812),
|
|
)
|
|
src_reg = AssetsComplementaryPoliceRegulation.objects.create(
|
|
title="Règlement Test",
|
|
reference="RCP-TEST-001",
|
|
)
|
|
src_reg.panels.add(src_panel)
|
|
|
|
# 2. Execute command
|
|
out = StringIO()
|
|
call_command("copy_sign_data", stdout=out)
|
|
output = out.getvalue()
|
|
self.assertIn("Copy of sign data completed successfully", output)
|
|
|
|
# 3. Assertions on copied destination models in Sign app
|
|
dst_ptype = SignPanelType.objects.filter(code="C1").first()
|
|
self.assertIsNotNone(dst_ptype)
|
|
self.assertEqual(dst_ptype.default_width, 60.0)
|
|
|
|
dst_pole = SignPole.objects.filter(code="POL-001").first()
|
|
self.assertIsNotNone(dst_pole)
|
|
self.assertEqual(dst_pole.street, adr_street)
|
|
self.assertEqual(dst_pole.organization, org)
|
|
|
|
dst_panel = SignPanel.objects.filter(code="PAN-001").first()
|
|
self.assertIsNotNone(dst_panel)
|
|
self.assertEqual(dst_panel.pole, dst_pole)
|
|
self.assertEqual(dst_panel.street, adr_street)
|
|
self.assertEqual(dst_panel.organization, org)
|
|
|
|
dst_reg = ComplementaryPoliceRegulation.objects.filter(reference="RCP-TEST-001").first()
|
|
self.assertIsNotNone(dst_reg)
|
|
self.assertIn(dst_panel, dst_reg.panels.all())
|
|
|
|
# 4. Verify idempotence
|
|
out2 = StringIO()
|
|
call_command("copy_sign_data", stdout=out2)
|
|
self.assertEqual(SignPole.objects.filter(code="POL-001").count(), 1)
|
|
self.assertEqual(SignPanel.objects.filter(code="PAN-001").count(), 1)
|
|
|
|
|
|
class SignStreetAssignmentTests(TestCase):
|
|
"""Tests for resolving AdrStreet and avoiding SignStreet assignment errors."""
|
|
|
|
def setUp(self):
|
|
from django.contrib.gis.geos import Point, LineString, Polygon, MultiPolygon
|
|
from common.models import Municipality, AdrStreet, AppView, UserConfig, Thematic, UserThematics
|
|
from assets.models import AssetCategory
|
|
from sign.models import (
|
|
SignPoleModel,
|
|
SignPanelModel,
|
|
SignPanelType,
|
|
SignStreet,
|
|
Organization,
|
|
)
|
|
|
|
self.client = Client()
|
|
|
|
# Create sign view and thematic
|
|
self.sign_view, _ = AppView.objects.get_or_create(
|
|
code='sign',
|
|
defaults={
|
|
'name_fr': 'Signalisation',
|
|
'name_nl': 'Signalisatie',
|
|
'url_name': 'sign:index',
|
|
'order': 2,
|
|
}
|
|
)
|
|
self.sign_thematic, _ = Thematic.objects.get_or_create(
|
|
code='sign',
|
|
defaults={
|
|
'name_fr': 'Signalisation',
|
|
'name_nl': 'Signalisatie',
|
|
'color': '#0022ff',
|
|
}
|
|
)
|
|
|
|
# Admin user with full edit access
|
|
self.user = User.objects.create_superuser(
|
|
username='admin_sign',
|
|
password='testpass123',
|
|
email='admin@test.com'
|
|
)
|
|
self.config = UserConfig.objects.create(user=self.user)
|
|
self.config.accessible_views.add(self.sign_view)
|
|
UserThematics.objects.create(
|
|
user_config=self.config,
|
|
thematic=self.sign_thematic,
|
|
can_view_assets=True,
|
|
can_edit_assets=True,
|
|
)
|
|
|
|
# Organization
|
|
self.org = Organization.objects.create(
|
|
code="GAN",
|
|
name_fr="Ganshoren",
|
|
name_nl="Ganshoren",
|
|
organization_type="municipality",
|
|
)
|
|
|
|
# Category
|
|
self.category = AssetCategory.objects.create(
|
|
code="sign_cat_test",
|
|
name_fr="Catégorie Signalisation Test",
|
|
)
|
|
|
|
# Municipality
|
|
self.muni = Municipality.objects.create(
|
|
fid="1083",
|
|
name_fr="Ganshoren",
|
|
name_nl="Ganshoren",
|
|
)
|
|
|
|
# AdrStreet (the actual address street in common)
|
|
poly = Polygon(((145000, 172000), (145000, 172500), (145500, 172500), (145500, 172000), (145000, 172000)))
|
|
self.adr_street = AdrStreet.objects.create(
|
|
fid="ADR-GAN-001",
|
|
name_fr="Avenue Charles-Quint",
|
|
name_nl="Keizer Karellaan",
|
|
municipality=self.muni,
|
|
geom=MultiPolygon(poly, srid=3812),
|
|
centroid=Point(145250, 172250, srid=3812),
|
|
)
|
|
|
|
# SignStreet (the road network geometry layer)
|
|
self.sign_street = SignStreet.objects.create(
|
|
code="GAN_R20_001",
|
|
name_fr="Avenue Charles-Quint",
|
|
name_nl="Keizer Karellaan",
|
|
municipality=self.muni,
|
|
geom=MultiPolygon(poly, srid=3812),
|
|
)
|
|
|
|
# Pole and panel models
|
|
self.pole_model = SignPoleModel.objects.create(
|
|
code="PM-TEST",
|
|
name_fr="Poteau Standard Test",
|
|
category=self.category,
|
|
height=3.0,
|
|
)
|
|
|
|
self.panel_model = SignPanelModel.objects.create(
|
|
code="PANM-TEST",
|
|
name_fr="Modèle C1 Test",
|
|
category=self.category,
|
|
shape="rond",
|
|
)
|
|
|
|
self.panel_type = SignPanelType.objects.create(
|
|
code="C1",
|
|
name_fr="Sens interdit",
|
|
name_nl="Verboden richting",
|
|
default_width=60.0,
|
|
default_height=60.0,
|
|
)
|
|
|
|
def test_get_closest_adr_street(self):
|
|
from sign.views import get_closest_adr_street
|
|
from django.contrib.gis.geos import Point
|
|
|
|
# Test resolving using SignStreet directly
|
|
adr = get_closest_adr_street(sign_street=self.sign_street)
|
|
self.assertEqual(adr, self.adr_street)
|
|
|
|
# Test resolving using spatial coords in Lambert 2008 (3812)
|
|
pt_3812 = Point(145250, 172250, srid=3812)
|
|
adr_pt = get_closest_adr_street(point=pt_3812)
|
|
self.assertEqual(adr_pt, self.adr_street)
|
|
|
|
def test_sign_pole_create_assigns_adr_street(self):
|
|
"""Test adding a pole at Charles-Quint does not crash with SignStreet assignment error."""
|
|
from django.contrib.gis.geos import Point
|
|
self.client.force_login(self.user)
|
|
pt = Point(145250, 172250, srid=3812)
|
|
pt.transform(4326)
|
|
|
|
response = self.client.post(reverse('sign:sign_pole_create'), {
|
|
'model': self.pole_model.id,
|
|
'organization': self.org.id,
|
|
'orientation': 0,
|
|
'lat': pt.y,
|
|
'lon': pt.x,
|
|
})
|
|
self.assertEqual(response.status_code, 200, response.content)
|
|
data = response.json()
|
|
self.assertTrue(data.get('success'))
|
|
|
|
from sign.models import SignPole
|
|
pole = SignPole.objects.get(id=data['id'])
|
|
# Must be an instance of AdrStreet, not SignStreet
|
|
from common.models import AdrStreet
|
|
self.assertIsInstance(pole.street, AdrStreet)
|
|
self.assertEqual(pole.street, self.adr_street)
|
|
|
|
def test_sign_panel_create_assigns_adr_street(self):
|
|
"""Test adding a panel at Charles-Quint assigns AdrStreet correctly."""
|
|
from django.contrib.gis.geos import Point
|
|
self.client.force_login(self.user)
|
|
pt = Point(145250, 172250, srid=3812)
|
|
pt.transform(4326)
|
|
|
|
response = self.client.post(reverse('sign:sign_panel_create'), {
|
|
'signpanel_type': self.panel_type.id,
|
|
'model': self.panel_model.id,
|
|
'organization': self.org.id,
|
|
'orientation': 0,
|
|
'support_type': 'pole',
|
|
'lat': pt.y,
|
|
'lon': pt.x,
|
|
})
|
|
self.assertEqual(response.status_code, 200, response.content)
|
|
data = response.json()
|
|
self.assertTrue(data.get('success'))
|
|
|
|
from sign.models import SignPanel
|
|
panel = SignPanel.objects.get(id=data['id'])
|
|
from common.models import AdrStreet
|
|
self.assertIsInstance(panel.street, AdrStreet)
|
|
self.assertEqual(panel.street, self.adr_street)
|
|
|
|
def test_sign_panel_create_attached_to_pole(self):
|
|
"""Test adding a panel attached to an existing pole inherits pole's street and sets vertical order."""
|
|
from django.contrib.gis.geos import Point
|
|
from sign.models import SignPole, SignPanel
|
|
from common.models import AdrStreet
|
|
|
|
pt = Point(145250, 172250, srid=3812)
|
|
pole = SignPole.objects.create(
|
|
street=self.adr_street,
|
|
model=self.pole_model,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.31,
|
|
lat=50.86,
|
|
)
|
|
|
|
self.client.force_login(self.user)
|
|
response = self.client.post(reverse('sign:sign_panel_create'), {
|
|
'pole_id': pole.id,
|
|
'signpanel_type': self.panel_type.id,
|
|
'model': self.panel_model.id,
|
|
'organization': self.org.id,
|
|
'orientation': 0,
|
|
'support_type': 'pole',
|
|
})
|
|
self.assertEqual(response.status_code, 200, response.content)
|
|
data = response.json()
|
|
self.assertTrue(data.get('success'))
|
|
|
|
panel = SignPanel.objects.get(id=data['id'])
|
|
self.assertEqual(panel.pole, pole)
|
|
self.assertEqual(panel.street, self.adr_street)
|
|
self.assertIsInstance(panel.street, AdrStreet)
|
|
self.assertEqual(panel.vertical_order, 1)
|
|
|
|
|
|
class SupportRepresentationAndModelTests(TestCase):
|
|
def setUp(self):
|
|
from common.models import Municipality, AdrStreet, UserConfig, AppView
|
|
from sign.models import SignPanelType, SignPoleModel, SignPanelModel, SignPole, SignPanel, Organization
|
|
from assets.models import TrafficLightIntersection, TrafficLightPoleModel, TrafficLightPole
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.gis.geos import Point
|
|
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(username="support_user", password="password", is_superuser=True)
|
|
sign_view, _ = AppView.objects.get_or_create(
|
|
code="sign",
|
|
defaults={
|
|
"name_fr": "Signalisation",
|
|
"name_nl": "Signalisatie",
|
|
"url_name": "sign:index",
|
|
}
|
|
)
|
|
self.config = UserConfig.objects.create(user=self.user, esign_can_edit=True)
|
|
self.config.accessible_views.add(sign_view)
|
|
|
|
self.muni = Municipality.objects.create(fid="1080", name_fr="Molenbeek", name_nl="Molenbeek")
|
|
self.adr_street = AdrStreet.objects.create(
|
|
fid="STR-100",
|
|
name_fr="Rue de Test",
|
|
name_nl="Teststraat",
|
|
municipality=self.muni,
|
|
)
|
|
self.org = Organization.objects.create(
|
|
code="TEST_ORG",
|
|
name_fr="Test Organisation",
|
|
name_nl="Test Organisatie",
|
|
organization_type="region",
|
|
)
|
|
self.pole_model = SignPoleModel.objects.create(
|
|
code="POL-MOD-1",
|
|
name_fr="Modèle Mât 4m",
|
|
)
|
|
self.panel_model = SignPanelModel.objects.create(
|
|
code="PAN-MOD-1",
|
|
name_fr="Modèle Type B",
|
|
)
|
|
self.panel_type = SignPanelType.objects.create(
|
|
code="B1",
|
|
name_fr="Céder le passage",
|
|
name_nl="Voorrang verlenen",
|
|
default_width=70.0,
|
|
default_height=70.0,
|
|
)
|
|
self.tl_intersection = TrafficLightIntersection.objects.create(
|
|
code="TL-INT-01",
|
|
name_fr="Carrefour Test",
|
|
)
|
|
self.tl_pole_model = TrafficLightPoleModel.objects.create(
|
|
code="TL-PM-01",
|
|
name_fr="Poteau Tricolore Acier 6m",
|
|
)
|
|
self.tl_pole = TrafficLightPole.objects.create(
|
|
code="TL-P-01",
|
|
intersection=self.tl_intersection,
|
|
model=self.tl_pole_model,
|
|
geom=Point(145000, 172000, srid=3812),
|
|
lon=4.32,
|
|
lat=50.85,
|
|
)
|
|
|
|
def test_signpole_support_type(self):
|
|
"""Test that SignPole supports support_type choices and defaults."""
|
|
from sign.models import SignPole
|
|
pole = SignPole.objects.create(
|
|
code="P-TEST-01",
|
|
street=self.adr_street,
|
|
organization=self.org,
|
|
support_type="trafficlightpole",
|
|
model=self.pole_model,
|
|
)
|
|
self.assertEqual(pole.support_type, "trafficlightpole")
|
|
self.assertEqual(pole.support_type_display, "Poteau de signalisation lumineuse")
|
|
|
|
def test_signpanel_effective_support_and_model(self):
|
|
"""Test effective_support_type and support_model_display on SignPanel."""
|
|
from sign.models import SignPole, SignPanel
|
|
|
|
# 1. Panel attached to pole with support_type
|
|
pole = SignPole.objects.create(
|
|
code="P-TEST-02",
|
|
street=self.adr_street,
|
|
organization=self.org,
|
|
support_type="lightingpole",
|
|
model=self.pole_model,
|
|
)
|
|
panel1 = SignPanel.objects.create(
|
|
code="PANEL-01",
|
|
pole=pole,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
)
|
|
self.assertEqual(panel1.effective_support_type, "lightingpole")
|
|
self.assertEqual(panel1.effective_support_type_display, "Poteau d'éclairage")
|
|
self.assertEqual(panel1.support_model_display, "Modèle Mât 4m")
|
|
|
|
# 2. Panel attached directly to trafficlight_pole
|
|
panel2 = SignPanel.objects.create(
|
|
code="PANEL-02",
|
|
trafficlight_pole=self.tl_pole,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
)
|
|
self.assertEqual(panel2.effective_support_type, "trafficlightpole")
|
|
self.assertEqual(panel2.effective_support_type_display, "Poteau de signalisation lumineuse")
|
|
self.assertEqual(panel2.support_model_display, "Poteau Tricolore Acier 6m")
|
|
|
|
# 3. Standalone panel with support_type and custom support_model
|
|
panel3 = SignPanel.objects.create(
|
|
code="PANEL-03",
|
|
support_type="trafficlightpole",
|
|
support_model="Feu Swarco Carrefour",
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
)
|
|
self.assertEqual(panel3.effective_support_type, "trafficlightpole")
|
|
self.assertEqual(panel3.effective_support_type_display, "Poteau de signalisation lumineuse")
|
|
self.assertEqual(panel3.support_model_display, "Feu Swarco Carrefour")
|
|
|
|
def test_sign_assets_geojson_includes_support_representation(self):
|
|
"""Test that sign_assets_geojson includes support_type, support_model and standalone support features."""
|
|
from django.contrib.gis.geos import Point
|
|
from sign.models import SignPanel
|
|
import json
|
|
|
|
pt = Point(145000, 172000, srid=3812)
|
|
panel = SignPanel.objects.create(
|
|
code="PANEL-TL-01",
|
|
street=self.adr_street,
|
|
support_type="trafficlightpole",
|
|
support_model="Feu Siemens 3 Feux",
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.32,
|
|
lat=50.85,
|
|
)
|
|
|
|
self.client.force_login(self.user)
|
|
response = self.client.post(
|
|
reverse('sign:sign_assets_geojson'),
|
|
data=json.dumps({'organization': [self.org.id]}),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
features = data.get('features', [])
|
|
self.assertTrue(len(features) >= 2)
|
|
|
|
panel_feat = next((f for f in features if f['properties'].get('asset_type') == 'signpanel'), None)
|
|
self.assertIsNotNone(panel_feat)
|
|
self.assertEqual(panel_feat['properties']['support_type'], 'trafficlightpole')
|
|
self.assertEqual(panel_feat['properties']['support_model'], 'Feu Siemens 3 Feux')
|
|
|
|
support_feat = next((f for f in features if f['properties'].get('asset_type') == 'signpole'), None)
|
|
self.assertIsNotNone(support_feat)
|
|
self.assertEqual(support_feat['properties']['support_type'], 'trafficlightpole')
|
|
self.assertEqual(support_feat['properties']['support_model'], 'Feu Siemens 3 Feux')
|
|
self.assertTrue(support_feat['properties'].get('is_standalone_support'))
|
|
|
|
def test_sign_poles_overview_geojson_includes_standalone_trafficlight(self):
|
|
"""Test sign_poles_overview_geojson includes standalone panel supports."""
|
|
from django.contrib.gis.geos import Point
|
|
from sign.models import SignPanel
|
|
import json
|
|
|
|
pt = Point(145000, 172000, srid=3812)
|
|
SignPanel.objects.create(
|
|
code="PANEL-TL-OV",
|
|
street=self.adr_street,
|
|
support_type="trafficlightpole",
|
|
support_model="Feu Carrefour 1",
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.32,
|
|
lat=50.85,
|
|
)
|
|
|
|
self.client.force_login(self.user)
|
|
response = self.client.post(
|
|
reverse('sign:sign_poles_overview_geojson'),
|
|
data=json.dumps({'organization': [self.org.id]}),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
features = data.get('features', [])
|
|
tl_support = next((f for f in features if f['properties'].get('support_type') == 'trafficlightpole'), None)
|
|
self.assertIsNotNone(tl_support)
|
|
self.assertEqual(tl_support['properties']['support_model'], 'Feu Carrefour 1')
|
|
self.assertTrue(tl_support['properties'].get('is_standalone_support'))
|
|
|
|
def test_sign_assets_geojson_excludes_inactive_statuses_by_default(self):
|
|
"""Verify that sign_assets_geojson excludes removed/archived panels by default, but includes them when filtered."""
|
|
from django.contrib.gis.geos import Point
|
|
from sign.models import SignPanel
|
|
import json
|
|
|
|
pt = Point(145000, 172000, srid=3812)
|
|
active_panel = SignPanel.objects.create(
|
|
code="PANEL-ACTIVE-01",
|
|
street=self.adr_street,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.32,
|
|
lat=50.85,
|
|
status="active",
|
|
)
|
|
archived_panel = SignPanel.objects.create(
|
|
code="PANEL-ARCHIVED-01",
|
|
street=self.adr_street,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.32,
|
|
lat=50.85,
|
|
status="archived",
|
|
)
|
|
|
|
self.client.force_login(self.user)
|
|
|
|
# Default query (no status specified): only active panel returned
|
|
resp = self.client.post(
|
|
reverse('sign:sign_assets_geojson'),
|
|
data=json.dumps({'organization': [self.org.id]}),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
features = resp.json().get('features', [])
|
|
codes = [f['properties']['code'] for f in features if f['properties'].get('asset_type') == 'signpanel']
|
|
self.assertIn("PANEL-ACTIVE-01", codes)
|
|
self.assertNotIn("PANEL-ARCHIVED-01", codes)
|
|
|
|
# Explicit query for archived status: archived panel returned
|
|
resp_archived = self.client.post(
|
|
reverse('sign:sign_assets_geojson'),
|
|
data=json.dumps({'organization': [self.org.id], 'asset_status': ['archived']}),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(resp_archived.status_code, 200)
|
|
features_archived = resp_archived.json().get('features', [])
|
|
codes_archived = [f['properties']['code'] for f in features_archived if f['properties'].get('asset_type') == 'signpanel']
|
|
self.assertIn("PANEL-ARCHIVED-01", codes_archived)
|
|
self.assertNotIn("PANEL-ACTIVE-01", codes_archived)
|
|
|
|
def test_sign_assets_geojson_suppresses_orphan_duplicate_at_pole(self):
|
|
"""Verify that sign_assets_geojson suppresses unlinked orphan panels that duplicate an attached panel on a pole."""
|
|
from django.contrib.gis.geos import Point
|
|
from sign.models import SignPole, SignPanel
|
|
import json
|
|
|
|
pt = Point(145000, 172000, srid=3812)
|
|
pole = SignPole.objects.create(
|
|
code="POLE-DUP-01",
|
|
street=self.adr_street,
|
|
organization=self.org,
|
|
model=self.pole_model,
|
|
geom=pt,
|
|
lon=4.35,
|
|
lat=50.85,
|
|
)
|
|
attached_panel = SignPanel.objects.create(
|
|
code="PANEL-ATT-01",
|
|
street=self.adr_street,
|
|
pole=pole,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.35,
|
|
lat=50.85,
|
|
status="active",
|
|
)
|
|
# Duplicate orphan panel at exact same coordinates with same signpanel_type
|
|
orphan_duplicate = SignPanel.objects.create(
|
|
code="PANEL-ORPH-01",
|
|
street=self.adr_street,
|
|
pole=None,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.35,
|
|
lat=50.85,
|
|
status="active",
|
|
)
|
|
|
|
self.client.force_login(self.user)
|
|
resp = self.client.post(
|
|
reverse('sign:sign_assets_geojson'),
|
|
data=json.dumps({'organization': [self.org.id]}),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
features = resp.json().get('features', [])
|
|
panel_features = [f for f in features if f['properties'].get('asset_type') == 'signpanel']
|
|
panel_codes = [f['properties']['code'] for f in panel_features]
|
|
|
|
# The attached panel is kept, the redundant orphan is suppressed
|
|
self.assertIn("PANEL-ATT-01", panel_codes)
|
|
self.assertNotIn("PANEL-ORPH-01", panel_codes)
|
|
|
|
def test_check_duplicate_sign_panels_command(self):
|
|
"""Verify that check_duplicate_sign_panels command detects and fixes duplicate orphan panels."""
|
|
from django.contrib.gis.geos import Point
|
|
from django.core.management import call_command
|
|
from io import StringIO
|
|
from sign.models import SignPole, SignPanel
|
|
|
|
pt = Point(145000, 172000, srid=3812)
|
|
pole = SignPole.objects.create(
|
|
code="POLE-CMD-01",
|
|
street=self.adr_street,
|
|
organization=self.org,
|
|
model=self.pole_model,
|
|
geom=pt,
|
|
lon=4.36,
|
|
lat=50.86,
|
|
)
|
|
SignPanel.objects.create(
|
|
code="PANEL-CMD-ATT",
|
|
street=self.adr_street,
|
|
pole=pole,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.36,
|
|
lat=50.86,
|
|
status="active",
|
|
)
|
|
orphan_panel = SignPanel.objects.create(
|
|
code="PANEL-CMD-ORPH",
|
|
street=self.adr_street,
|
|
pole=None,
|
|
model=self.panel_model,
|
|
signpanel_type=self.panel_type,
|
|
organization=self.org,
|
|
geom=pt,
|
|
lon=4.36,
|
|
lat=50.86,
|
|
status="active",
|
|
)
|
|
|
|
# 1. Dry run
|
|
out = StringIO()
|
|
call_command("check_duplicate_sign_panels", org_id=self.org.id, stdout=out)
|
|
output = out.getvalue()
|
|
self.assertIn("Doublons orphelins redondants détectés : 1", output)
|
|
orphan_panel.refresh_from_db()
|
|
self.assertEqual(orphan_panel.status, "active")
|
|
|
|
# 2. Fix mode (archive)
|
|
out_fix = StringIO()
|
|
call_command("check_duplicate_sign_panels", org_id=self.org.id, fix=True, action="archive", stdout=out_fix)
|
|
fix_output = out_fix.getvalue()
|
|
self.assertIn("1 doublon(s) orphelin(s) archives", fix_output.replace("é", "e"))
|
|
orphan_panel.refresh_from_db()
|
|
self.assertEqual(orphan_panel.status, "archived")
|
|
|
|
|
|
class SignPanelCategoryFilterTest(TestCase):
|
|
"""Tests for panel type category grouping and filter controls."""
|
|
|
|
def setUp(self):
|
|
from django.contrib.auth.models import User
|
|
from common.models import UserConfig, AppView
|
|
from sign.models import SignPanelType
|
|
|
|
self.user = User.objects.create_superuser(username="filter_test_user", password="password123", email="filter@test.com")
|
|
self.sign_view, _ = AppView.objects.get_or_create(
|
|
code='sign',
|
|
defaults={
|
|
'name_fr': 'Signalisation',
|
|
'name_nl': 'Signalisatie',
|
|
'url_name': 'sign:index',
|
|
}
|
|
)
|
|
self.user_config = UserConfig.objects.create(user=self.user)
|
|
self.user_config.accessible_views.add(self.sign_view)
|
|
|
|
# Create panel types across different categories
|
|
self.type_danger_1 = SignPanelType.objects.create(code="A1a", name_fr="Virage à droite", name_nl="Bocht rechts")
|
|
self.type_danger_2 = SignPanelType.objects.create(code="A1b", name_fr="Virage à gauche", name_nl="Bocht links")
|
|
self.type_priority = SignPanelType.objects.create(code="B1", name_fr="Céder le passage", name_nl="Voorrang verlenen")
|
|
self.type_prohibition = SignPanelType.objects.create(code="C1", name_fr="Sens interdit", name_nl="Verboden richting")
|
|
|
|
def test_get_grouped_panel_types(self):
|
|
"""Verify get_grouped_panel_types categorizes panel types according to Belgian traffic code."""
|
|
from sign.views import get_grouped_panel_types
|
|
|
|
grouped, groups = get_grouped_panel_types()
|
|
|
|
# Check group labels exist
|
|
group_keys = [g[0] for g in groups]
|
|
self.assertIn("danger", group_keys)
|
|
self.assertIn("priority", group_keys)
|
|
self.assertIn("prohibition", group_keys)
|
|
|
|
# Check danger group contains A1a and A1b
|
|
danger_codes = [p['code'] for p in grouped['danger']]
|
|
self.assertIn("A1a", danger_codes)
|
|
self.assertIn("A1b", danger_codes)
|
|
|
|
# Check priority contains B1
|
|
priority_codes = [p['code'] for p in grouped['priority']]
|
|
self.assertIn("B1", priority_codes)
|
|
|
|
def test_index_view_renders_category_filter_controls(self):
|
|
"""Verify the sign index view renders the category select-all checkboxes and quick-action toolbar."""
|
|
self.client.force_login(self.user)
|
|
response = self.client.get(reverse('sign:index'))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
content = response.content.decode('utf-8')
|
|
|
|
# Verify accordion item with group key
|
|
self.assertIn('data-group-key="danger"', content)
|
|
# Verify group select-all checkbox
|
|
self.assertIn('class="form-check-input group-select-all m-0"', content)
|
|
self.assertIn('id="check-group-danger"', content)
|
|
self.assertIn('data-group="danger"', content)
|
|
# Verify count badge
|
|
self.assertIn('id="badge-danger"', content)
|
|
# Verify toolbar buttons: Tout cocher / Tout décocher
|
|
self.assertIn('select-all-btn', content)
|
|
self.assertIn('deselect-all-btn', content)
|
|
# Verify data-group attribute on panel item
|
|
self.assertIn('data-group="danger"', content)
|