Repository: nautobot/nautobot

Issue:
Filtering By Related Tree Models With Non-Unique Names Can Return Unexpected Results
              Additional unexpected behavior transforms filtering of parent locations into the parent name and the name of all descendants in the tree (as opposed to the actual descendants in the tree). For example, having the following Location Tree:

```
Region 1
  Site 1
Region 2
  Site 1
  Site 2
```

and using the filter `{"location": ["Region 2"]}`

Would result in `location__name__in=["Region 2", "Site 1", "Site 2"]`

I would expect this to be `location__in=[<LocationRegion1>,<LocationSite1>,<LocationSite2>]`

_Originally posted by @jmcgill298 in https://github.com/nautobot/nautobot/issues/4207#issuecomment-1693821232_
            


Discussion:
Example: 
<img width="1492" alt="image" src="https://github.com/nautobot/nautobot/assets/31187/90fdcfd4-f458-4ef4-a04f-8f0f7ecbbb57">


Resolving patch:
diff --git a/nautobot/core/filters.py b/nautobot/core/filters.py
--- a/nautobot/core/filters.py
+++ b/nautobot/core/filters.py
@@ -500,8 +500,9 @@ def generate_query(self, value, qs=None, **kwargs):
         # Construct a list of filter predicates that will be used to generate the Q object.
         predicates = []
         for obj in value:
-            # Try to get the `to_field_name` (e.g. `name`) or just pass the object through.
-            val = getattr(obj, self.field.to_field_name, obj)
+            # Get the exact instance by PK as we are nested from the original query,
+            #   or just pass the object through, commonly the null case.
+            val = getattr(obj, "pk", obj)
             if val == self.null_value:
                 val = None
             predicates.append(self.get_filter_predicate(val))


Test patch:
diff --git a/nautobot/core/tests/test_filters.py b/nautobot/core/tests/test_filters.py
--- a/nautobot/core/tests/test_filters.py
+++ b/nautobot/core/tests/test_filters.py
@@ -87,26 +87,60 @@ def setUp(self):
             location_type=self.location_type,
             status=status,
         )
+        # To ensure child objects with parents of the same name are not included in the results for filtering,
+        # we create parents with the same name under different ancestors.
+        self.parent_location_same_name_1 = dcim_models.Location.objects.create(
+            parent=self.child_location_1,
+            name="Test Parent Location Same Name",
+            location_type=self.location_type,
+            status=status,
+        )
+        self.parent_location_same_name_2 = dcim_models.Location.objects.create(
+            parent=self.child_location_2ab,
+            name="Test Parent Location Same Name",
+            location_type=self.location_type,
+            status=status,
+        )
+        self.child_location_same_name_1 = dcim_models.Location.objects.create(
+            parent=self.parent_location_same_name_1,
+            name="Test Child Location Same Name Parent 1",
+            location_type=self.location_type,
+            status=status,
+        )
+        self.child_location_same_name_2 = dcim_models.Location.objects.create(
+            parent=self.parent_location_same_name_2,
+            name="Test Child Location Same Name Parent 2",
+            location_type=self.location_type,
+            status=status,
+        )
         self.queryset = dcim_models.Location.objects.filter(name__icontains="Test Child Location")
 
     def test_filter_single_name(self):
         kwargs = {"parent": [self.parent_location_1.name]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
-        self.assertQuerysetEqual(qs, [self.child_location_1])
+        self.assertQuerysetEqual(qs, [self.child_location_1, self.child_location_same_name_1])
 
     def test_filter_single_pk(self):
         kwargs = {"parent": [self.parent_location_1.pk]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
-        self.assertQuerysetEqual(qs, [self.child_location_1])
+        self.assertQuerysetEqual(qs, [self.child_location_1, self.child_location_same_name_1])
 
     def test_filter_multiple_name(self):
         kwargs = {"parent": [self.parent_location_1.name, self.parent_location_2.name]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
         self.assertQuerysetEqual(
-            qs, [self.child_location_1, self.child_location_2, self.child_location_2a, self.child_location_2ab]
+            qs,
+            [
+                self.child_location_1,
+                self.child_location_same_name_1,
+                self.child_location_2,
+                self.child_location_2a,
+                self.child_location_2ab,
+                self.child_location_same_name_2,
+            ],
         )
 
     def test_filter_null(self):
@@ -119,14 +153,21 @@ def test_filter_combined_name(self):
         kwargs = {"parent": [self.parent_location_1.name, settings.FILTERS_NULL_CHOICE_VALUE]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
-        self.assertQuerysetEqual(qs, [self.child_location_0, self.child_location_1])
+        self.assertQuerysetEqual(qs, [self.child_location_0, self.child_location_1, self.child_location_same_name_1])
 
     def test_filter_combined_pk(self):
         kwargs = {"parent": [self.parent_location_2.pk, settings.FILTERS_NULL_CHOICE_VALUE]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
         self.assertQuerysetEqual(
-            qs, [self.child_location_0, self.child_location_2, self.child_location_2a, self.child_location_2ab]
+            qs,
+            [
+                self.child_location_0,
+                self.child_location_2,
+                self.child_location_2a,
+                self.child_location_2ab,
+                self.child_location_same_name_2,
+            ],
         )
 
     def test_filter_single_name_exclude(self):
@@ -134,14 +175,21 @@ def test_filter_single_name_exclude(self):
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
         self.assertQuerysetEqual(
-            qs, [self.child_location_0, self.child_location_2, self.child_location_2a, self.child_location_2ab]
+            qs,
+            [
+                self.child_location_0,
+                self.child_location_2,
+                self.child_location_2a,
+                self.child_location_2ab,
+                self.child_location_same_name_2,
+            ],
         )
 
     def test_filter_single_pk_exclude(self):
         kwargs = {"parent__n": [self.parent_location_2.pk]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
-        self.assertQuerysetEqual(qs, [self.child_location_0, self.child_location_1])
+        self.assertQuerysetEqual(qs, [self.child_location_0, self.child_location_1, self.child_location_same_name_1])
 
     def test_filter_multiple_name_exclude(self):
         kwargs = {"parent__n": [self.parent_location_1.name, self.parent_location_2.name]}
@@ -154,20 +202,31 @@ def test_filter_null_exclude(self):
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
         self.assertQuerysetEqual(
-            qs, [self.child_location_1, self.child_location_2, self.child_location_2a, self.child_location_2ab]
+            qs,
+            [
+                self.child_location_1,
+                self.child_location_same_name_1,
+                self.child_location_2,
+                self.child_location_2a,
+                self.child_location_2ab,
+                self.child_location_same_name_2,
+            ],
         )
 
     def test_filter_combined_name_exclude(self):
         kwargs = {"parent__n": [self.parent_location_1.name, settings.FILTERS_NULL_CHOICE_VALUE]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
-        self.assertQuerysetEqual(qs, [self.child_location_2, self.child_location_2a, self.child_location_2ab])
+        self.assertQuerysetEqual(
+            qs,
+            [self.child_location_2, self.child_location_2a, self.child_location_2ab, self.child_location_same_name_2],
+        )
 
     def test_filter_combined_pk_exclude(self):
         kwargs = {"parent__n": [self.parent_location_2.pk, settings.FILTERS_NULL_CHOICE_VALUE]}
         qs = self.LocationFilterSet(kwargs, self.queryset).qs
 
-        self.assertQuerysetEqual(qs, [self.child_location_1])
+        self.assertQuerysetEqual(qs, [self.child_location_1, self.child_location_same_name_1])
 
     def test_lookup_expr_param_ignored(self):
         """