diff --git a/loko/interventions/models.py b/loko/interventions/models.py index adb62a1..fc11261 100644 --- a/loko/interventions/models.py +++ b/loko/interventions/models.py @@ -742,6 +742,61 @@ class Intervention(models.Model): status__in=['to_process', 'pending'] ).exists() + def get_associated_observations(self): + """ + Retourne le QuerySet de toutes les observations associées à cette intervention : + - Celles dont le champ intervention pointe directement vers cette intervention + - Celles correspondant au source_ref de cette intervention + - Celles associées aux interventions liées via InterventionLink (origine, duplication, réparation) + """ + if not self.pk: + from observations.models import Observation + return Observation.objects.none() + + from observations.models import Observation + + possible_obs_ids = set() + for obs_id in self.observations.values_list('id', flat=True): + possible_obs_ids.add(obs_id) + + if self.source_category and self.source_ref: + sc_names = [getattr(self.source_category, a, '') or '' for a in ('name_fr', 'name_nl')] + if any(n.strip().lower() == 'observation' for n in sc_names): + try: + possible_obs_ids.add(int(self.source_ref)) + except (ValueError, TypeError): + pass + if self.source_ref.startswith('OBS') and self.source_ref[3:].isdigit(): + try: + possible_obs_ids.add(int(self.source_ref[3:])) + except (ValueError, TypeError): + pass + + # Interventions liées (duplication, réparation, lien manuel) + linked_itv_ids = set() + for f, s in InterventionLink.objects.filter( + Q(first=self) | Q(second=self) + ).values_list('first_id', 'second_id'): + linked_itv_ids.add(f if f != self.id else s) + + if linked_itv_ids: + for obs_id in Observation.objects.filter(intervention_id__in=linked_itv_ids).values_list('id', flat=True): + possible_obs_ids.add(obs_id) + for itv_other in Intervention.objects.filter(id__in=linked_itv_ids, source_category__isnull=False).exclude(source_ref__isnull=True).exclude(source_ref=''): + sc_names = [getattr(itv_other.source_category, a, '') or '' for a in ('name_fr', 'name_nl')] + if any(n.strip().lower() == 'observation' for n in sc_names): + try: + possible_obs_ids.add(int(itv_other.source_ref)) + except (ValueError, TypeError): + pass + if itv_other.source_ref.startswith('OBS') and itv_other.source_ref[3:].isdigit(): + try: + possible_obs_ids.add(int(itv_other.source_ref[3:])) + except (ValueError, TypeError): + pass + + return Observation.objects.filter(id__in=possible_obs_ids).order_by('-created_at') + class InterventionLink(models.Model): class LinkType(models.TextChoices): diff --git a/loko/interventions/tests/test_general.py b/loko/interventions/tests/test_general.py index 4edad0b..e538936 100644 --- a/loko/interventions/tests/test_general.py +++ b/loko/interventions/tests/test_general.py @@ -1550,15 +1550,56 @@ class InterventionDuplicationTests(TestCase): self.assertIsNotNone(new_itv) self.assertRedirects(response, reverse('mobile:intervention_detail_mobile', kwargs={'intervention_id': new_itv.id})) - def test_duplication_unauthorized(self): - User = get_user_model() - unauth_user = User.objects.create_user(username='unauth', password='pwd') - UserConfig.objects.create(user=unauth_user, is_intern=False) + def test_duplication_with_associated_observation(self): + from observations.models import Observation + from interventions.models import SourceCategory + source_cat, _ = SourceCategory.objects.get_or_create(name_fr='Observation', defaults={'name_nl': 'Observatie'}) + + # Create an observation linked to original intervention + obs = Observation.objects.create( + thematic=self.thematic, + status='in_progress', + created_by=self.admin_user, + description="Test Observation with Intervention", + intervention=self.itv, + latitude=50.85, + longitude=4.35, + ) + self.itv.source_category = source_cat + self.itv.source_ref = str(obs.id) + self.itv.save() - self.client.force_login(unauth_user) + # Check initial associations + self.assertIn(self.itv, obs.get_associated_interventions()) + self.assertIn(obs, self.itv.get_associated_observations()) + + # Duplicate intervention + self.client.force_login(self.admin_user) url = reverse('interventions:duplicate_intervention', kwargs={'intervention_id': self.itv.id}) response = self.client.post(url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 302) + + new_itv = Intervention.objects.exclude(id=self.itv.id).first() + self.assertIsNotNone(new_itv) + + # Both interventions should be associated with the observation + obs_interventions = list(obs.get_associated_interventions()) + self.assertIn(self.itv, obs_interventions) + self.assertIn(new_itv, obs_interventions) + + # Both interventions should retrieve the observation + self.assertIn(obs, self.itv.get_associated_observations()) + self.assertIn(obs, new_itv.get_associated_observations()) + + # Check observation detail view displays visible_interventions + obs_url = reverse('observations:observation_detail', kwargs={'observation_id': obs.id}) + obs_response = self.client.get(obs_url) + self.assertEqual(obs_response.status_code, 200) + self.assertIn('visible_interventions', obs_response.context) + visible_itv_ids = [item['intervention'].id for item in obs_response.context['visible_interventions']] + self.assertIn(self.itv.id, visible_itv_ids) + self.assertIn(new_itv.id, visible_itv_ids) + diff --git a/loko/interventions/views/detail.py b/loko/interventions/views/detail.py index 692dd0a..60cf679 100644 --- a/loko/interventions/views/detail.py +++ b/loko/interventions/views/detail.py @@ -545,7 +545,7 @@ def intervention_detail(request, intervention_id): linked_observations = [] obs_access_context = get_observation_access_context(request.user, user_config=user_config) if obs_access_context: - all_observations = intervention.observations.select_related('thematic', 'category').order_by('-created_at') + all_observations = intervention.get_associated_observations().select_related('thematic', 'category') linked_observations = [obs for obs in all_observations if obs_access_context.allows(obs)] # Observation source (si la source est une observation) diff --git a/loko/interventions/views/mobile.py b/loko/interventions/views/mobile.py index 1e13de8..7c24143 100644 --- a/loko/interventions/views/mobile.py +++ b/loko/interventions/views/mobile.py @@ -1043,7 +1043,7 @@ def intervention_detail_mobile(request, intervention_id): linked_observations = [] obs_access_context = get_observation_access_context(request.user, user_config=user_config) if obs_access_context: - all_observations = intervention.observations.select_related('thematic', 'category').order_by('-created_at') + all_observations = intervention.get_associated_observations().select_related('thematic', 'category') linked_observations = [obs for obs in all_observations if obs_access_context.allows(obs)] # Discussion liée à cette intervention (mobile) diff --git a/loko/observations/models.py b/loko/observations/models.py index cf46ea3..8270c3c 100644 --- a/loko/observations/models.py +++ b/loko/observations/models.py @@ -460,6 +460,43 @@ class Observation(models.Model): models.Q(observations=self) | models.Q(source_ref__in=possible_refs) ).exists() + def get_associated_interventions(self): + """ + Retourne le QuerySet de toutes les interventions associées à cette observation : + - L'intervention directement liée (self.intervention) + - Les interventions ayant cette observation en source (source_ref / source_category) + - Les interventions liées (InterventionLink) issues de ces interventions (duplications, réparations). + """ + if not self.pk: + from interventions.models import Intervention + return Intervention.objects.none() + + from interventions.models import Intervention, InterventionLink + possible_refs = [str(self.pk)] + if self.code: + possible_refs.append(self.code) + possible_refs.append(f"OBS{self.pk:05d}") + + direct_ids = set( + Intervention.objects.filter( + models.Q(observations=self) | models.Q(source_ref__in=possible_refs) + ).values_list('id', flat=True) + ) + if self.intervention_id: + direct_ids.add(self.intervention_id) + + all_ids = set(direct_ids) + if direct_ids: + linked_ids = set() + for first_id, second_id in InterventionLink.objects.filter( + models.Q(first_id__in=direct_ids) | models.Q(second_id__in=direct_ids) + ).values_list('first_id', 'second_id'): + linked_ids.add(first_id) + linked_ids.add(second_id) + all_ids.update(linked_ids) + + return Intervention.objects.filter(id__in=all_ids).order_by('-creation_time', '-id') + def save(self, *args, **kwargs): is_new = self.pk is None old_status = None diff --git a/loko/observations/templates/observations/observation_detail.html b/loko/observations/templates/observations/observation_detail.html index 8d547a6..8ba4fc7 100644 --- a/loko/observations/templates/observations/observation_detail.html +++ b/loko/observations/templates/observations/observation_detail.html @@ -304,8 +304,65 @@ {% endif %} - - {% if observation.intervention %} + + {% if visible_interventions %} +
+
+
+ + {% if visible_interventions|length > 1 %} + {% translate "Interventions associées" %} + {% else %} + {% translate "Intervention liée" %} + {% endif %} + {{ visible_interventions|length }} +
+ {% if can_link and observation.intervention %} +
+ {% csrf_token %} + + +
+ {% endif %} +
+
+ {% for item in visible_interventions %} + {% with itv=item.intervention %} +
+
+ {% if item.can_view_detail %} + + {{ itv.code|default:itv.id }} + + {% else %} + {{ itv.code|default:itv.id }} + {% endif %} + – {{ itv.title }} +
+
+ + {{ itv.get_status_display }} + + {% if item.can_view_summary %} + + {% endif %} +
+
+ {% endwith %} + {% endfor %} +
+
+ {% elif observation.intervention %}
{% translate "Intervention liée" %}
diff --git a/loko/observations/templates/observations/observation_detail_mobile.html b/loko/observations/templates/observations/observation_detail_mobile.html index 4f3dfe6..ef2c673 100644 --- a/loko/observations/templates/observations/observation_detail_mobile.html +++ b/loko/observations/templates/observations/observation_detail_mobile.html @@ -364,8 +364,54 @@
{% endif %} - -{% if observation.intervention %} + +{% if visible_interventions %} +
+
+ + {% if visible_interventions|length > 1 %} + {% translate "Interventions associées" %} + {% else %} + {% translate "Intervention liée" %} + {% endif %} + {{ visible_interventions|length }} +
+
+ {% for item in visible_interventions %} + {% with itv=item.intervention %} +
+
+ {% if item.can_view_detail %} + + {{ itv.code|default:itv.id }} + + {% else %} + {{ itv.code|default:itv.id }} + {% endif %} + {{ itv.title }} +
+
+ + {{ itv.get_status_display }} + + {% if item.can_view_summary %} + + {% endif %} +
+
+ {% endwith %} + {% endfor %} +
+
+{% elif observation.intervention %}
{% translate "Intervention liée" %}
@@ -390,7 +436,6 @@ {% else %} {{ observation.intervention.code|default:observation.intervention.id }} {% endif %} -
{% endif %} diff --git a/loko/observations/views.py b/loko/observations/views.py index d3adbc2..fa85da6 100644 --- a/loko/observations/views.py +++ b/loko/observations/views.py @@ -747,12 +747,24 @@ def observation_detail(request, observation_id): observation.status in ('in_preparation', 'to_process') and can_edit ) + + # Récupérer toutes les interventions associées (directes, sources, duplications/réparations) + from interventions.permissions import can_view_intervention, can_view_intervention_summary + associated_interventions = observation.get_associated_interventions() + visible_interventions = [] + for itv in associated_interventions: + if can_view_intervention(request.user, itv): + visible_interventions.append({ + 'intervention': itv, + 'can_view_detail': can_view_intervention(request.user, itv), + 'can_view_summary': can_view_intervention_summary(request.user, itv), + 'is_primary': (itv.id == observation.intervention_id), + }) - # Vérifier les permissions pour l'intervention liée + # Vérifier les permissions pour l'intervention liée principale can_view_intervention_detail = False can_view_intervention_summary_modal = False if observation.intervention: - from interventions.permissions import can_view_intervention, can_view_intervention_summary can_view_intervention_detail = can_view_intervention(request.user, observation.intervention) can_view_intervention_summary_modal = can_view_intervention_summary(request.user, observation.intervention) @@ -862,6 +874,7 @@ def observation_detail(request, observation_id): 'is_creator': is_creator, 'can_view_intervention_detail': can_view_intervention_detail, 'can_view_intervention_summary_modal': can_view_intervention_summary_modal, + 'visible_interventions': visible_interventions, 'discussion': discussion, 'discussion_messages': discussion_messages, 'can_post_discussion_message': can_post_discussion_message,