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 %} +