- Add can_view_all_posts and can_view_all_prices boolean fields to UserConfig - Add migration 0020 with automatic data migration for Legal Office users - Update permissions helpers, UserConfigAdmin, copy_user_config and intervention detail view - Add unit tests for global posts and prices permissions
759 lines
31 KiB
Python
759 lines
31 KiB
Python
from django.test import TestCase, Client
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
from django.urls import reverse
|
|
import json
|
|
|
|
from contracts.models import Company, CompanyTeam, CompanyMember, CompanyMemberHistory, Contract
|
|
from common.models import UserConfig, Role
|
|
|
|
User = get_user_model()
|
|
|
|
class CompanyMemberTeamsTests(TestCase):
|
|
def setUp(self):
|
|
# Create user and configuration
|
|
self.user = User.objects.create_user(username="testuser", password="password123")
|
|
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
|
|
|
|
# Create company and teams
|
|
self.company = Company.objects.create(name="Test Company")
|
|
self.team1 = CompanyTeam.objects.create(name="Team Alpha", company=self.company)
|
|
self.team2 = CompanyTeam.objects.create(name="Team Beta", company=self.company)
|
|
|
|
# Create member
|
|
self.member = CompanyMember.objects.create(
|
|
name="Jean Dupont",
|
|
user=self.user
|
|
)
|
|
|
|
def test_assign_multiple_teams(self):
|
|
"""Test that a member can be assigned to multiple teams and history tracks it."""
|
|
today = timezone.now().date()
|
|
|
|
# Add to Team 1
|
|
self.member.teams.add(self.team1)
|
|
self.assertEqual(self.member.teams.count(), 1)
|
|
|
|
# Verify history entry created
|
|
histories = CompanyMemberHistory.objects.filter(member=self.member, team=self.team1)
|
|
self.assertEqual(histories.count(), 1)
|
|
self.assertEqual(histories.first().join_date, today)
|
|
self.assertIsNone(histories.first().leave_date)
|
|
|
|
# Add to Team 2
|
|
self.member.teams.add(self.team2)
|
|
self.assertEqual(self.member.teams.count(), 2)
|
|
|
|
# Verify history for Team 2
|
|
histories_team2 = CompanyMemberHistory.objects.filter(member=self.member, team=self.team2)
|
|
self.assertEqual(histories_team2.count(), 1)
|
|
self.assertEqual(histories_team2.first().join_date, today)
|
|
self.assertIsNone(histories_team2.first().leave_date)
|
|
|
|
def test_remove_team(self):
|
|
"""Test that removing a team closes the history period."""
|
|
today = timezone.now().date()
|
|
self.member.teams.add(self.team1)
|
|
|
|
# Remove from Team 1
|
|
self.member.teams.remove(self.team1)
|
|
self.assertEqual(self.member.teams.count(), 0)
|
|
|
|
# Verify history leave_date updated
|
|
history = CompanyMemberHistory.objects.get(member=self.member, team=self.team1)
|
|
self.assertEqual(history.leave_date, today)
|
|
|
|
def test_clear_teams(self):
|
|
"""Test that clearing teams closes all active history periods."""
|
|
today = timezone.now().date()
|
|
self.member.teams.add(self.team1)
|
|
self.member.teams.add(self.team2)
|
|
|
|
# Clear teams
|
|
self.member.teams.clear()
|
|
self.assertEqual(self.member.teams.count(), 0)
|
|
|
|
# Verify history leave_date updated for all
|
|
h1 = CompanyMemberHistory.objects.get(member=self.member, team=self.team1)
|
|
h2 = CompanyMemberHistory.objects.get(member=self.member, team=self.team2)
|
|
self.assertEqual(h1.leave_date, today)
|
|
self.assertEqual(h2.leave_date, today)
|
|
|
|
|
|
class TeamViewsAjaxTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(username="adminuser", password="password")
|
|
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
|
|
# Give admin role to bypass permission denials
|
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
|
self.user_config.roles.add(self.admin_role)
|
|
|
|
self.client.login(username="adminuser", password="password")
|
|
|
|
self.company = Company.objects.create(name="Test Company")
|
|
self.team_a = CompanyTeam.objects.create(name="Team A", company=self.company)
|
|
self.team_b = CompanyTeam.objects.create(name="Team B", company=self.company)
|
|
|
|
self.member = CompanyMember.objects.create(name="Tech A", user=self.user)
|
|
self.member.teams.add(self.team_a)
|
|
today = timezone.now().date()
|
|
yesterday = today - timezone.timedelta(days=1)
|
|
CompanyMemberHistory.objects.filter(member=self.member, team=self.team_a).update(join_date=yesterday)
|
|
|
|
def test_teams_move_member_transfer(self):
|
|
"""Test moving a member transfers them (removes old, adds new)."""
|
|
url = reverse('contracts:teams_move_member', args=[self.member.id])
|
|
payload = {
|
|
'old_team_id': self.team_a.id,
|
|
'team_id': self.team_b.id
|
|
}
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(payload),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.member.refresh_from_db()
|
|
# Should only belong to Team B now
|
|
self.assertEqual(list(self.member.teams.all()), [self.team_b])
|
|
|
|
# Verify history was updated
|
|
today = timezone.now().date()
|
|
yesterday = today - timezone.timedelta(days=1)
|
|
h_old = CompanyMemberHistory.objects.get(member=self.member, team=self.team_a)
|
|
self.assertEqual(h_old.leave_date, yesterday)
|
|
h_new = CompanyMemberHistory.objects.get(member=self.member, team=self.team_b)
|
|
self.assertEqual(h_new.join_date, today)
|
|
self.assertIsNone(h_new.leave_date)
|
|
|
|
def test_teams_update_member_modal(self):
|
|
"""Test updating a member via modal view with transfer."""
|
|
url = reverse('contracts:teams_update_member', args=[self.member.id])
|
|
payload = {
|
|
'name': 'Tech A Modified',
|
|
'old_team_id': self.team_a.id,
|
|
'team_id': self.team_b.id
|
|
}
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(payload),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.member.refresh_from_db()
|
|
self.assertEqual(self.member.name, 'Tech A Modified')
|
|
self.assertEqual(list(self.member.teams.all()), [self.team_b])
|
|
|
|
def test_teams_create_member_different_company_allowed_for_team_company(self):
|
|
"""Test that a non-admin user can add a member belonging to the target team's company, even if different from user's company."""
|
|
from django.contrib.auth.models import User
|
|
from common.models import UserConfig, Role
|
|
from contracts.models import Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess
|
|
|
|
# Create user of Company A
|
|
other_user = User.objects.create_user(username="otheruser", password="password")
|
|
company_a = Company.objects.create(name="Company A")
|
|
other_user_config = UserConfig.objects.create(user=other_user, company=company_a)
|
|
|
|
# Create team belonging to Company B
|
|
company_b = Company.objects.create(name="Company B")
|
|
team_b = CompanyTeam.objects.create(name="Team Company B", company=company_b)
|
|
|
|
# Grant user edit permission on team_b
|
|
UserCompanyTeamAccess.objects.create(
|
|
user_config=other_user_config,
|
|
team=team_b,
|
|
can_view=True,
|
|
can_edit=True
|
|
)
|
|
|
|
# Create target user to add, belonging to Company B
|
|
target_user = User.objects.create_user(username="targetuser", password="password")
|
|
UserConfig.objects.create(user=target_user, company=company_b)
|
|
|
|
# Login other_user
|
|
self.client.logout()
|
|
self.client.login(username="otheruser", password="password")
|
|
|
|
# Call teams_create_member
|
|
url = reverse('contracts:teams_create_member')
|
|
payload = {
|
|
'name': 'New Target Member',
|
|
'user_id': target_user.id,
|
|
'team_id': team_b.id
|
|
}
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(payload),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Verify member was created and added to team_b
|
|
member = CompanyMember.objects.get(user=target_user)
|
|
self.assertEqual(member.name, 'New Target Member')
|
|
self.assertIn(team_b, member.teams.all())
|
|
|
|
|
|
class ContractDisplayNameTests(TestCase):
|
|
def setUp(self):
|
|
from datetime import date
|
|
from contracts.models import Contract, Company
|
|
self.company = Company.objects.create(name="Company Tests")
|
|
self.contract_no_desc = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-001",
|
|
start_date=date(2025, 1, 1),
|
|
end_date=date(2026, 1, 1)
|
|
)
|
|
self.contract_short_desc = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-002",
|
|
description="Entretien espaces verts",
|
|
start_date=date(2025, 1, 1),
|
|
end_date=date(2026, 1, 1)
|
|
)
|
|
self.contract_long_desc = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-003",
|
|
description="Travaux et maintenance préventive et corrective de la signalisation lumineuse tricolore",
|
|
start_date=date(2025, 1, 1),
|
|
end_date=date(2026, 1, 1)
|
|
)
|
|
|
|
def test_display_name_no_description(self):
|
|
self.assertEqual(self.contract_no_desc.display_name, "CTR-001")
|
|
|
|
def test_display_name_short_description(self):
|
|
self.assertEqual(self.contract_short_desc.display_name, "CTR-002 (Entretien espaces verts)")
|
|
|
|
def test_display_name_long_description_truncated(self):
|
|
self.assertTrue(self.contract_long_desc.display_name.startswith("CTR-003 (Travaux et maintenance préventive et correct"))
|
|
self.assertTrue(self.contract_long_desc.display_name.endswith("…)"))
|
|
|
|
|
|
class GenerateClaimDeclarationTimelineTests(TestCase):
|
|
def setUp(self):
|
|
from decimal import Decimal
|
|
from datetime import date
|
|
from contracts.models import Contract, ContractOrder, ContractPost
|
|
from common.models import UserContractAccess
|
|
from interventions.models import Intervention, InterventionTimeLine, InterventionContractPost, InterventionPostPeriodQuantity
|
|
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(username="admin_user", password="password")
|
|
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
|
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
|
self.user_config.roles.add(self.admin_role)
|
|
self.client.login(username="admin_user", password="password")
|
|
|
|
self.company = Company.objects.create(name="Company A")
|
|
self.contract = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-TEST-001",
|
|
start_date=date(2025, 1, 1),
|
|
end_date=date(2026, 1, 1),
|
|
)
|
|
UserContractAccess.objects.create(
|
|
user_config=self.user_config,
|
|
contract=self.contract,
|
|
can_view_interventions=True,
|
|
)
|
|
self.order = ContractOrder.objects.create(
|
|
contract=self.contract,
|
|
order_code="ORD-TEST-01",
|
|
order_status="pending",
|
|
order_date=date(2025, 1, 1),
|
|
delivery_date=date(2025, 1, 15),
|
|
description="Commande test",
|
|
)
|
|
self.post = ContractPost.objects.create(
|
|
contract=self.contract,
|
|
order_number=1,
|
|
code="P1",
|
|
description_fr="Poste 1",
|
|
description_nl="Post 1",
|
|
unit="u",
|
|
unit_price=Decimal("10.00"),
|
|
)
|
|
self.intervention_validated = Intervention.objects.create(
|
|
title="Intervention Validated",
|
|
contract=self.contract,
|
|
order=self.order,
|
|
status="validated",
|
|
)
|
|
InterventionContractPost.objects.create(
|
|
intervention=self.intervention_validated,
|
|
contract_post=self.post,
|
|
quantity=Decimal("2.00"),
|
|
control_status="validated",
|
|
)
|
|
|
|
self.intervention_period = Intervention.objects.create(
|
|
title="Intervention with Validated Period",
|
|
contract=self.contract,
|
|
order=self.order,
|
|
status="to_be_processed",
|
|
)
|
|
InterventionPostPeriodQuantity.objects.create(
|
|
intervention=self.intervention_period,
|
|
contract_post=self.post,
|
|
period_number=1,
|
|
quantity=Decimal("3.00"),
|
|
validated_at=timezone.now(),
|
|
validated_by=self.user,
|
|
status="validated",
|
|
)
|
|
|
|
def test_generate_documents_creates_timeline_events(self):
|
|
from interventions.models import InterventionTimeLine
|
|
|
|
url = reverse("contracts:generate_documents_for_order", args=[self.order.id])
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
# 1. Check validated intervention status & timeline
|
|
self.intervention_validated.refresh_from_db()
|
|
self.assertEqual(self.intervention_validated.status, "invoiced")
|
|
|
|
timeline_event_1 = InterventionTimeLine.objects.filter(
|
|
intervention=self.intervention_validated,
|
|
to_status="invoiced",
|
|
).first()
|
|
self.assertIsNotNone(timeline_event_1)
|
|
self.assertEqual(timeline_event_1.from_status, "validated")
|
|
self.assertEqual(timeline_event_1.event_type, "status_change")
|
|
self.assertEqual(timeline_event_1.event_user, self.user)
|
|
|
|
# 2. Check intervention with period status & timeline
|
|
self.intervention_period.refresh_from_db()
|
|
self.assertEqual(self.intervention_period.status, "invoiced")
|
|
|
|
timeline_event_2 = InterventionTimeLine.objects.filter(
|
|
intervention=self.intervention_period,
|
|
to_status="invoiced",
|
|
).first()
|
|
self.assertIsNotNone(timeline_event_2)
|
|
self.assertEqual(timeline_event_2.from_status, "to_be_processed")
|
|
self.assertEqual(timeline_event_2.event_type, "status_change")
|
|
self.assertEqual(timeline_event_2.event_user, self.user)
|
|
|
|
|
|
class ContractOrderDetailControllerDisplayTests(TestCase):
|
|
def setUp(self):
|
|
from datetime import date
|
|
from contracts.models import Contract, ContractOrder, Company, CompanyTeam, CompanyMember
|
|
from common.models import UserContractAccess
|
|
from interventions.models import Intervention
|
|
|
|
self.client = Client()
|
|
self.user = User.objects.create_user(username="order_admin_user", password="password")
|
|
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
|
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
|
self.user_config.roles.add(self.admin_role)
|
|
self.client.login(username="order_admin_user", password="password")
|
|
|
|
self.company = Company.objects.create(name="Société Test Contrôle")
|
|
self.team_ctrl = CompanyTeam.objects.create(
|
|
name="Équipe Contrôle Nord",
|
|
company=self.company,
|
|
operation_types=[CompanyTeam.OPERATION_CONTROLS],
|
|
)
|
|
ctrl_user = User.objects.create_user(username="ctrl_member_user", password="password")
|
|
self.controller_member = CompanyMember.objects.create(
|
|
user=ctrl_user,
|
|
name="Jean Contrôleur",
|
|
)
|
|
self.controller_member.teams.add(self.team_ctrl)
|
|
|
|
self.contract_with_ctrl = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-CTRL-001",
|
|
start_date=date(2025, 1, 1),
|
|
end_date=date(2026, 1, 1),
|
|
auto_assign_control_team=True,
|
|
)
|
|
UserContractAccess.objects.create(
|
|
user_config=self.user_config,
|
|
contract=self.contract_with_ctrl,
|
|
can_view_interventions=True,
|
|
)
|
|
self.order_with_ctrl = ContractOrder.objects.create(
|
|
contract=self.contract_with_ctrl,
|
|
order_code="ORD-CTRL-01",
|
|
order_status="pending",
|
|
order_date=date(2025, 1, 1),
|
|
delivery_date=date(2025, 1, 15),
|
|
description="Commande avec marché contrôlé",
|
|
)
|
|
self.itv1 = Intervention.objects.create(
|
|
code="ITV-C01",
|
|
title="Intervention avec contrôleur",
|
|
contract=self.contract_with_ctrl,
|
|
order=self.order_with_ctrl,
|
|
assigned_control_team=self.team_ctrl,
|
|
assigned_controller=self.controller_member,
|
|
status="finished",
|
|
)
|
|
self.itv2 = Intervention.objects.create(
|
|
code="ITV-C02",
|
|
title="Intervention équipe seule",
|
|
contract=self.contract_with_ctrl,
|
|
order=self.order_with_ctrl,
|
|
assigned_control_team=self.team_ctrl,
|
|
status="processed",
|
|
)
|
|
|
|
self.contract_without_ctrl = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-NO-CTRL-002",
|
|
start_date=date(2025, 1, 1),
|
|
end_date=date(2026, 1, 1),
|
|
auto_assign_control_team=False,
|
|
)
|
|
UserContractAccess.objects.create(
|
|
user_config=self.user_config,
|
|
contract=self.contract_without_ctrl,
|
|
can_view_interventions=True,
|
|
)
|
|
self.order_without_ctrl = ContractOrder.objects.create(
|
|
contract=self.contract_without_ctrl,
|
|
order_code="ORD-NO-CTRL-02",
|
|
order_status="pending",
|
|
order_date=date(2025, 1, 1),
|
|
delivery_date=date(2025, 1, 15),
|
|
description="Commande sans marché contrôlé",
|
|
)
|
|
self.itv3 = Intervention.objects.create(
|
|
code="ITV-NC01",
|
|
title="Intervention classique",
|
|
contract=self.contract_without_ctrl,
|
|
order=self.order_without_ctrl,
|
|
status="finished",
|
|
)
|
|
|
|
def test_order_detail_shows_controller_when_contract_has_auto_assign_control_team(self):
|
|
url = reverse("contracts:contracts_orders_detail", kwargs={"order_id": self.order_with_ctrl.id})
|
|
response = self.client.get(url, HTTP_ACCEPT_LANGUAGE='fr')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Check th exists
|
|
self.assertContains(response, 'data-field="assigned_controller"')
|
|
self.assertContains(response, 'Contrôleur assigné')
|
|
|
|
# Check td contents
|
|
self.assertContains(response, 'Jean Contrôleur')
|
|
self.assertContains(response, 'Équipe Contrôle Nord')
|
|
|
|
def test_order_detail_hides_controller_when_contract_does_not_have_auto_assign_control_team(self):
|
|
url = reverse("contracts:contracts_orders_detail", kwargs={"order_id": self.order_without_ctrl.id})
|
|
response = self.client.get(url, HTTP_ACCEPT_LANGUAGE='fr')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# Check th does not exist
|
|
self.assertNotContains(response, 'data-field="assigned_controller"')
|
|
self.assertNotContains(response, 'Contrôleur assigné')
|
|
|
|
|
|
|
|
class ContractGeometryTests(TestCase):
|
|
def setUp(self):
|
|
self.company = Company.objects.create(name="ETA Environnement")
|
|
|
|
def test_contract_with_donut_geometry_and_color(self):
|
|
"""Test that Contract accepts a MultiPolygon with interior ring (donut hole) and color."""
|
|
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
|
|
from datetime import date
|
|
|
|
outer_ring = ((0, 0), (100, 0), (100, 100), (0, 100), (0, 0))
|
|
hole = ((20, 20), (40, 20), (40, 40), (20, 40), (20, 20))
|
|
poly_with_hole = Polygon(outer_ring, hole)
|
|
multi_poly = MultiPolygon([poly_with_hole], srid=3812)
|
|
|
|
contract = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-ETA-LOT1",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
geom=multi_poly,
|
|
color="#28a745",
|
|
)
|
|
|
|
self.assertEqual(contract.color, "#28a745")
|
|
self.assertIsNotNone(contract.geom)
|
|
self.assertEqual(contract.geom.srid, 3812)
|
|
self.assertEqual(contract.geom.geom_type, "MultiPolygon")
|
|
|
|
# GeoJSON property
|
|
geojson = contract.geojson_geometry
|
|
self.assertIsNotNone(geojson)
|
|
self.assertEqual(geojson["type"], "MultiPolygon")
|
|
self.assertEqual(len(geojson["coordinates"]), 1)
|
|
self.assertEqual(len(geojson["coordinates"][0]), 2)
|
|
|
|
# Spatial queries with hole
|
|
pt_inside = Point(10, 10, srid=3812)
|
|
pt_in_hole = Point(30, 30, srid=3812)
|
|
|
|
self.assertTrue(Contract.objects.filter(pk=contract.pk, geom__intersects=pt_inside).exists())
|
|
self.assertFalse(Contract.objects.filter(pk=contract.pk, geom__intersects=pt_in_hole).exists())
|
|
|
|
def test_contract_admin_has_geom(self):
|
|
"""Test ContractAdmin has_geom method."""
|
|
from django.contrib.admin.sites import AdminSite
|
|
from contracts.admin import ContractAdmin
|
|
from datetime import date
|
|
|
|
admin_instance = ContractAdmin(Contract, AdminSite())
|
|
contract_without_geom = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-NOGEOM",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
)
|
|
self.assertFalse(admin_instance.has_geom(contract_without_geom))
|
|
|
|
|
|
class ContractGeometryTests(TestCase):
|
|
def setUp(self):
|
|
self.company = Company.objects.create(name="ETA Environnement")
|
|
|
|
def test_contract_with_donut_geometry_and_color(self):
|
|
"""Test that Contract accepts a MultiPolygon with interior ring (donut hole) and color."""
|
|
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
|
|
from datetime import date
|
|
|
|
# Polygon with an outer ring and an inner ring (hole)
|
|
# Outer ring: (0,0) to (100, 100)
|
|
# Inner hole: (20,20) to (40, 40)
|
|
outer_ring = ((0, 0), (100, 0), (100, 100), (0, 100), (0, 0))
|
|
hole = ((20, 20), (40, 20), (40, 40), (20, 40), (20, 20))
|
|
poly_with_hole = Polygon(outer_ring, hole)
|
|
multi_poly = MultiPolygon([poly_with_hole], srid=3812)
|
|
|
|
contract = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-ETA-LOT1",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
geom=multi_poly,
|
|
color="#28a745",
|
|
)
|
|
|
|
self.assertEqual(contract.color, "#28a745")
|
|
self.assertIsNotNone(contract.geom)
|
|
self.assertEqual(contract.geom.srid, 3812)
|
|
self.assertEqual(contract.geom.geom_type, "MultiPolygon")
|
|
|
|
# GeoJSON property
|
|
geojson = contract.geojson_geometry
|
|
self.assertIsNotNone(geojson)
|
|
self.assertEqual(geojson["type"], "MultiPolygon")
|
|
# 1 polygon in multipolygon, having 2 rings (outer + hole)
|
|
self.assertEqual(len(geojson["coordinates"]), 1)
|
|
self.assertEqual(len(geojson["coordinates"][0]), 2)
|
|
|
|
# Spatial queries with hole
|
|
# Point inside outer ring but outside hole -> intersects
|
|
pt_inside = Point(10, 10, srid=3812)
|
|
# Point inside the hole -> does NOT intersect
|
|
pt_in_hole = Point(30, 30, srid=3812)
|
|
|
|
self.assertTrue(Contract.objects.filter(pk=contract.pk, geom__intersects=pt_inside).exists())
|
|
self.assertFalse(Contract.objects.filter(pk=contract.pk, geom__intersects=pt_in_hole).exists())
|
|
|
|
def test_contract_admin_has_geom(self):
|
|
"""Test ContractAdmin has_geom method."""
|
|
from django.contrib.admin.sites import AdminSite
|
|
from contracts.admin import ContractAdmin
|
|
from datetime import date
|
|
|
|
admin_instance = ContractAdmin(Contract, AdminSite())
|
|
contract_without_geom = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-NOGEOM",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
)
|
|
self.assertFalse(admin_instance.has_geom(contract_without_geom))
|
|
|
|
|
|
class ContractGeojsonEndpointTests(TestCase):
|
|
def setUp(self):
|
|
from django.contrib.auth import get_user_model
|
|
from common.models import Thematic, Role, UserConfig, UserThematics, UserContractAccess
|
|
from django.contrib.gis.geos import Polygon, MultiPolygon
|
|
from datetime import date
|
|
|
|
User = get_user_model()
|
|
self.user = User.objects.create_user(username="test_nature_user", password="password")
|
|
self.role_manager = Role.objects.create(name="manager")
|
|
self.config = UserConfig.objects.create(user=self.user)
|
|
self.config.roles.add(self.role_manager)
|
|
|
|
self.thematic_nature = Thematic.objects.create(code="nature", name_fr="Nature", name_nl="Natuur")
|
|
self.thematic_roads = Thematic.objects.create(code="roads", name_fr="Voiries", name_nl="Wegen")
|
|
|
|
UserThematics.objects.create(
|
|
user_config=self.config,
|
|
thematic=self.thematic_nature,
|
|
can_view_assets=True,
|
|
can_view_interventions=True
|
|
)
|
|
|
|
self.company = Company.objects.create(name="ETA Vert")
|
|
|
|
poly = Polygon(((0, 0), (10, 0), (10, 10), (0, 10), (0, 0)))
|
|
self.multi_poly = MultiPolygon([poly], srid=3812)
|
|
|
|
# Nature contract with geom
|
|
self.contract_nature_with_geom = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-NAT-01",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
geom=self.multi_poly,
|
|
color="#198754",
|
|
description="Lot 1 entretien espaces verts",
|
|
)
|
|
self.contract_nature_with_geom.thematics.add(self.thematic_nature)
|
|
|
|
# Nature contract without geom
|
|
self.contract_nature_no_geom = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-NAT-NOGEOM",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
)
|
|
self.contract_nature_no_geom.thematics.add(self.thematic_nature)
|
|
|
|
# Roads contract with geom
|
|
self.contract_roads_with_geom = Contract.objects.create(
|
|
company=self.company,
|
|
contract_number="CTR-ROADS-01",
|
|
start_date=date(2026, 1, 1),
|
|
end_date=date(2028, 12, 31),
|
|
geom=self.multi_poly,
|
|
color="#0d6efd",
|
|
)
|
|
self.contract_roads_with_geom.thematics.add(self.thematic_roads)
|
|
|
|
# Grant access to nature contracts
|
|
UserContractAccess.objects.create(
|
|
user_config=self.config,
|
|
contract=self.contract_nature_with_geom,
|
|
can_view_assets=True,
|
|
can_view_interventions=True
|
|
)
|
|
UserContractAccess.objects.create(
|
|
user_config=self.config,
|
|
contract=self.contract_nature_no_geom,
|
|
can_view_assets=True,
|
|
can_view_interventions=True
|
|
)
|
|
|
|
def test_anonymous_access_forbidden(self):
|
|
url = reverse("contracts:contracts_geojson")
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_authenticated_nature_contracts_geojson(self):
|
|
self.client.login(username="test_nature_user", password="password")
|
|
url = reverse("contracts:contracts_geojson") + "?thematic=nature"
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
data = response.json()
|
|
self.assertEqual(data["type"], "FeatureCollection")
|
|
# Only 1 feature should be returned (nature with geom)
|
|
self.assertEqual(len(data["features"]), 1)
|
|
feat = data["features"][0]
|
|
self.assertEqual(feat["id"], self.contract_nature_with_geom.id)
|
|
self.assertEqual(feat["properties"]["contract_number"], "CTR-NAT-01")
|
|
self.assertEqual(feat["properties"]["company_name"], "ETA Vert")
|
|
self.assertEqual(feat["properties"]["color"], "#198754")
|
|
self.assertEqual(feat["properties"]["description"], "Lot 1 entretien espaces verts")
|
|
self.assertEqual(feat["geometry"]["type"], "MultiPolygon")
|
|
|
|
def test_superuser_access_all_contracts(self):
|
|
from django.contrib.auth import get_user_model
|
|
User = get_user_model()
|
|
admin = User.objects.create_superuser(username="admin_user", password="password", email="admin@example.com")
|
|
self.client.login(username="admin_user", password="password")
|
|
|
|
url = reverse("contracts:contracts_geojson") + "?thematic=nature"
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(len(data["features"]), 1)
|
|
|
|
# For roads:
|
|
url_roads = reverse("contracts:contracts_geojson") + "?thematic=roads"
|
|
response_roads = self.client.get(url_roads)
|
|
self.assertEqual(response_roads.status_code, 200)
|
|
data_roads = response_roads.json()
|
|
self.assertEqual(len(data_roads["features"]), 1)
|
|
self.assertEqual(data_roads["features"][0]["properties"]["contract_number"], "CTR-ROADS-01")
|
|
|
|
def test_user_without_contract_access_gets_empty(self):
|
|
from django.contrib.auth import get_user_model
|
|
from common.models import Role, UserConfig, UserThematics
|
|
User = get_user_model()
|
|
other_user = User.objects.create_user(username="other_user", password="password")
|
|
config = UserConfig.objects.create(user=other_user)
|
|
role = Role.objects.get(name="manager")
|
|
config.roles.add(role)
|
|
UserThematics.objects.create(
|
|
user_config=config,
|
|
thematic=self.thematic_nature,
|
|
can_view_assets=True
|
|
)
|
|
|
|
self.client.login(username="other_user", password="password")
|
|
url = reverse("contracts:contracts_geojson") + "?thematic=nature"
|
|
response = self.client.get(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(len(data["features"]), 0)
|
|
|
|
|
|
class GlobalPostsAndPricesPermissionsTests(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="viewer_user", password="password123")
|
|
self.role_viewer, _ = Role.objects.get_or_create(name="viewer")
|
|
self.config = UserConfig.objects.create(
|
|
user=self.user,
|
|
can_view_all_posts=False,
|
|
can_view_all_prices=False,
|
|
)
|
|
self.config.roles.add(self.role_viewer)
|
|
self.contract = Contract.objects.create(
|
|
contract_number="CTR-TEST-001",
|
|
description="Test Contract"
|
|
)
|
|
|
|
def test_default_viewer_cannot_view_posts_or_prices(self):
|
|
from contracts.permissions import user_can_view_contract_prices, user_has_access_to_contract_posts
|
|
self.assertFalse(user_can_view_contract_prices(self.user, self.contract.id))
|
|
self.assertFalse(user_has_access_to_contract_posts(self.user, self.contract.id))
|
|
|
|
def test_viewer_with_can_view_all_posts(self):
|
|
from contracts.permissions import user_has_access_to_contract_posts
|
|
self.config.can_view_all_posts = True
|
|
self.config.save()
|
|
self.assertTrue(user_has_access_to_contract_posts(self.user, self.contract.id))
|
|
|
|
def test_viewer_with_can_view_all_prices(self):
|
|
from contracts.permissions import user_can_view_contract_prices
|
|
self.config.can_view_all_prices = True
|
|
self.config.save()
|
|
self.assertTrue(user_can_view_contract_prices(self.user, self.contract.id))
|
|
|
|
|
|
|