nautobot 2.4.0__py3-none-any.whl → 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (40) hide show
  1. nautobot/core/celery/schedulers.py +1 -1
  2. nautobot/core/filters.py +48 -21
  3. nautobot/core/jobs/bulk_actions.py +56 -19
  4. nautobot/core/models/__init__.py +2 -0
  5. nautobot/core/tables.py +5 -1
  6. nautobot/core/testing/filters.py +25 -13
  7. nautobot/core/testing/integration.py +86 -4
  8. nautobot/core/tests/test_filters.py +209 -246
  9. nautobot/core/tests/test_jobs.py +250 -93
  10. nautobot/core/tests/test_models.py +9 -0
  11. nautobot/core/views/generic.py +80 -48
  12. nautobot/core/views/mixins.py +34 -6
  13. nautobot/dcim/api/serializers.py +2 -2
  14. nautobot/dcim/constants.py +6 -13
  15. nautobot/dcim/factory.py +6 -1
  16. nautobot/dcim/tests/integration/test_device_bulk_delete.py +189 -0
  17. nautobot/dcim/tests/integration/test_device_bulk_edit.py +181 -0
  18. nautobot/dcim/tests/test_api.py +0 -2
  19. nautobot/dcim/tests/test_models.py +42 -28
  20. nautobot/extras/forms/mixins.py +1 -1
  21. nautobot/extras/jobs.py +15 -6
  22. nautobot/extras/templatetags/job_buttons.py +4 -4
  23. nautobot/extras/tests/test_forms.py +13 -0
  24. nautobot/extras/tests/test_jobs.py +18 -13
  25. nautobot/extras/tests/test_models.py +6 -0
  26. nautobot/extras/tests/test_views.py +4 -3
  27. nautobot/ipam/tests/test_api.py +20 -0
  28. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +36 -1
  29. nautobot/project-static/docs/objects.inv +0 -0
  30. nautobot/project-static/docs/release-notes/version-2.4.html +108 -0
  31. nautobot/project-static/docs/search/search_index.json +1 -1
  32. nautobot/project-static/docs/sitemap.xml +288 -288
  33. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  34. nautobot/wireless/tests/test_views.py +22 -1
  35. {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/METADATA +2 -2
  36. {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/RECORD +40 -38
  37. {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/LICENSE.txt +0 -0
  38. {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/NOTICE +0 -0
  39. {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/WHEEL +0 -0
  40. {nautobot-2.4.0.dist-info → nautobot-2.4.1.dist-info}/entry_points.txt +0 -0
@@ -8,6 +8,7 @@ from django.core.files.base import ContentFile
8
8
  from django.utils import timezone
9
9
  import yaml
10
10
 
11
+ from nautobot.circuits.models import Circuit, CircuitType, Provider
11
12
  from nautobot.core.jobs.cleanup import CleanupTypes
12
13
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
13
14
  from nautobot.core.testing.context import load_event_broker_override_settings
@@ -27,7 +28,7 @@ from nautobot.extras.models import (
27
28
  Tag,
28
29
  )
29
30
  from nautobot.extras.models.metadata import ObjectMetadata
30
- from nautobot.ipam.models import Namespace, Prefix
31
+ from nautobot.ipam.models import IPAddress, Namespace, Prefix
31
32
  from nautobot.users.models import ObjectPermission
32
33
 
33
34
 
@@ -619,6 +620,7 @@ class BulkEditTestCase(TransactionTestCase):
619
620
  self.status_ct = ContentType.objects.get_for_model(Status)
620
621
  self.namespace_ct = ContentType.objects.get_for_model(Namespace)
621
622
  self.role_ct = ContentType.objects.get_for_model(Role)
623
+ self.ipaddress_ct = ContentType.objects.get_for_model(IPAddress)
622
624
  self.tags = [Tag.objects.create(name=f"Example Tag {x}") for x in range(5)]
623
625
  for tag in self.tags:
624
626
  tag.content_types.add(self.namespace_ct)
@@ -633,7 +635,7 @@ class BulkEditTestCase(TransactionTestCase):
633
635
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR).exists()
634
636
  )
635
637
 
636
- def test_bulk_edit_without_permission(self):
638
+ def test_bulk_edit_objects_without_permission(self):
637
639
  statuses = [Status.objects.create(name=f"Sample Status {x}") for x in range(3)]
638
640
  pk_list = [str(status.id) for status in statuses]
639
641
  job_result = create_job_result_and_run_job(
@@ -654,45 +656,72 @@ class BulkEditTestCase(TransactionTestCase):
654
656
  status.refresh_from_db()
655
657
  self.assertNotEqual(status.color, "aa1409")
656
658
 
657
- def test_bulk_edit_all(self):
658
- self.add_permissions("extras.change_role", "extras.view_role")
659
+ def test_bulk_edit_objects_with_constrained_permission(self):
660
+ roles_to_update = [
661
+ Role.objects.create(name="Example Role 1"),
662
+ Role.objects.create(name="Example Role 2"),
663
+ ]
664
+ pk_list = [str(role.pk) for role in roles_to_update]
665
+
666
+ obj_perm = ObjectPermission.objects.create(
667
+ name="Test permission",
668
+ constraints={"pk": pk_list[0]},
669
+ actions=["change", "view"],
670
+ )
671
+ obj_perm.users.add(self.user)
672
+ obj_perm.object_types.add(self.role_ct)
673
+
659
674
  job_result = create_job_result_and_run_job(
660
675
  "nautobot.core.jobs.bulk_actions",
661
676
  "BulkEditObjects",
662
677
  content_type=self.role_ct.id,
663
- edit_all=True,
664
- filter_query_params={},
665
- form_data={"color": "aa1409"},
678
+ edit_all=False,
679
+ filter_query_params={"per_page": 2},
680
+ form_data={"pk": pk_list, "color": "aa1409"},
666
681
  username=self.user.username,
667
682
  )
668
- self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), color="aa1409")
683
+ error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
684
+ self.assertIn("Form validation unsuccessful", error_log.message)
685
+ self.assertIn(f"{roles_to_update[1].pk} is not one of the available choices.", error_log.message)
686
+ roles_to_update[0].refresh_from_db()
687
+ roles_to_update[1].refresh_from_db()
688
+ self.assertNotEqual(roles_to_update[0].color, "aa1409")
689
+ self.assertNotEqual(roles_to_update[1].color, "aa1409")
690
+
691
+ obj_perm.constraints = {"pk__in": pk_list}
692
+ obj_perm.save()
669
693
 
670
- def test_bulk_edit_filter_all(self):
671
- self.add_permissions("extras.change_status", "extras.view_status")
672
- # By default Active and Available are some of the example of Status that starts with A
673
- statuses = Status.objects.filter(name__istartswith="A")
674
- status_to_ignore = Status.objects.create(name="Ignore Example Status")
675
- self.assertNotEqual(statuses.count(), 0)
676
694
  job_result = create_job_result_and_run_job(
677
695
  "nautobot.core.jobs.bulk_actions",
678
696
  "BulkEditObjects",
679
- content_type=self.status_ct.id,
680
- edit_all=True,
681
- filter_query_params={"name__isw": "A"},
682
- # pk ignored if edit_all is True
683
- form_data={
684
- "pk": [str(statuses[0].pk)],
685
- "color": "aa1409",
686
- "_all": "True",
687
- },
697
+ content_type=self.role_ct.id,
698
+ edit_all=False,
699
+ filter_query_params={"sort": "name"},
700
+ form_data={"pk": pk_list, "color": "aa1409"},
688
701
  username=self.user.username,
689
702
  )
690
- self._common_no_error_test_assertion(
691
- Status, job_result, statuses.count(), name__istartswith="A", color="aa1409"
703
+ self._common_no_error_test_assertion(Role, job_result, 2, pk__in=pk_list, color="aa1409")
704
+
705
+ def test_bulk_edit_objects_select_all(self):
706
+ """
707
+ Bulk edit all Role instances.
708
+ """
709
+ self.add_permissions("extras.change_role", "extras.view_role")
710
+ job_result = create_job_result_and_run_job(
711
+ "nautobot.core.jobs.bulk_actions",
712
+ "BulkEditObjects",
713
+ content_type=self.role_ct.id,
714
+ edit_all=True,
715
+ filter_query_params={},
716
+ form_data={"color": "aa1409"},
717
+ username=self.user.username,
692
718
  )
693
- self.assertNotEqual(status_to_ignore.color, "aa1409")
719
+ self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), color="aa1409")
694
720
 
695
- def test_bulk_edit_with_pk(self):
721
+ def test_bulk_edit_select_some(self):
722
+ """
723
+ Bulk edit selected Namespace instances.
724
+ """
696
725
  self.add_permissions("ipam.change_namespace", "ipam.view_namespace", "extras.change_tag", "extras.view_tag")
697
726
  namespaces = [Namespace.objects.create(name=f"Sample Namespace {x}") for x in range(5)]
698
727
  for namespace in namespaces:
@@ -731,63 +760,170 @@ class BulkEditTestCase(TransactionTestCase):
731
760
  self.assertFalse(namespace.tags.filter(pk__in=[tag.pk for tag in self.tags[:3]]).exists())
732
761
  self.assertTrue(namespace.tags.filter(pk__in=[tag.pk for tag in self.tags[3:]]).exists())
733
762
 
734
- def test_bulk_edit_objects_with_constrained_permission(self):
735
- roles_to_update = [
736
- Role.objects.create(name="Example Role 1"),
737
- Role.objects.create(name="Example Role 2"),
738
- ]
739
- pk_list = [str(role.pk) for role in roles_to_update]
740
-
741
- obj_perm = ObjectPermission.objects.create(
742
- name="Test permission",
743
- constraints={"pk": pk_list[0]},
744
- actions=["change", "view"],
763
+ def test_bulk_edit_objects_filter_all(self):
764
+ """
765
+ Bulk edit all of the filtered Status instances.
766
+ """
767
+ self.add_permissions("extras.change_status", "extras.view_status")
768
+ # By default Active and Available are some of the example of Status that starts with A
769
+ statuses = Status.objects.filter(name__istartswith="A")
770
+ status_to_ignore = Status.objects.create(name="Ignore Example Status")
771
+ self.assertNotEqual(statuses.count(), 0)
772
+ job_result = create_job_result_and_run_job(
773
+ "nautobot.core.jobs.bulk_actions",
774
+ "BulkEditObjects",
775
+ content_type=self.status_ct.id,
776
+ edit_all=True,
777
+ filter_query_params={"name__isw": "A"},
778
+ form_data={
779
+ "color": "aa1409",
780
+ "_all": "True",
781
+ },
782
+ username=self.user.username,
745
783
  )
746
- obj_perm.users.add(self.user)
747
- obj_perm.object_types.add(self.role_ct)
784
+ self._common_no_error_test_assertion(
785
+ Status, job_result, statuses.count(), name__istartswith="A", color="aa1409"
786
+ )
787
+ self.assertNotEqual(status_to_ignore.color, "aa1409")
748
788
 
789
+ def test_bulk_edit_objects_filter_some(self):
790
+ """
791
+ Bulk edit some of the filtered Status instances.
792
+ """
793
+ self.add_permissions("extras.change_status", "extras.view_status")
794
+ # By default Active and Available are some of the example of Status that starts with A
795
+ statuses = Status.objects.filter(name__istartswith="A")
796
+ status_to_ignore = Status.objects.create(name="Ignore Example Status")
797
+ self.assertNotEqual(statuses.count(), 0)
749
798
  job_result = create_job_result_and_run_job(
750
799
  "nautobot.core.jobs.bulk_actions",
751
800
  "BulkEditObjects",
752
- content_type=self.role_ct.id,
801
+ content_type=self.status_ct.id,
753
802
  edit_all=False,
754
- filter_query_params={},
755
- form_data={"pk": pk_list, "color": "aa1409"},
803
+ filter_query_params={"name__isw": "A", "sort": "name"},
804
+ form_data={
805
+ "pk": [str(statuses[0].pk)],
806
+ "color": "aa1409",
807
+ },
756
808
  username=self.user.username,
757
809
  )
758
- error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
759
- self.assertIn("Form validation unsuccessful", error_log.message)
760
- self.assertIn(f"{roles_to_update[1].pk} is not one of the available choices.", error_log.message)
761
- roles_to_update[0].refresh_from_db()
762
- roles_to_update[1].refresh_from_db()
763
- self.assertNotEqual(roles_to_update[0].color, "aa1409")
764
- self.assertNotEqual(roles_to_update[1].color, "aa1409")
765
-
766
- obj_perm.constraints = {"pk__in": pk_list}
767
- obj_perm.save()
810
+ self._common_no_error_test_assertion(Status, job_result, 1, name__istartswith="A", color="aa1409")
811
+ self.assertNotEqual(status_to_ignore.color, "aa1409")
768
812
 
813
+ def test_bulk_edit_objects_passing_in_both_pk_list_and_edit_all(self):
814
+ """
815
+ edit_all should override pk if both are passed.
816
+ """
817
+ self.add_permissions("extras.change_status", "extras.view_status")
818
+ # By default Active and Available are some of the example of Status that starts with A
819
+ statuses = Status.objects.filter(name__istartswith="A")
820
+ status_to_ignore = Status.objects.create(name="Ignore Example Status")
821
+ self.assertNotEqual(statuses.count(), 0)
769
822
  job_result = create_job_result_and_run_job(
770
823
  "nautobot.core.jobs.bulk_actions",
771
824
  "BulkEditObjects",
772
- content_type=self.role_ct.id,
773
- edit_all=False,
774
- filter_query_params={},
775
- form_data={"pk": pk_list, "color": "aa1409"},
825
+ content_type=self.status_ct.id,
826
+ edit_all=True,
827
+ filter_query_params={"name__isw": "A"},
828
+ # pk ignored if edit_all is True
829
+ form_data={
830
+ "pk": [str(statuses[0].pk)],
831
+ "color": "aa1409",
832
+ "_all": "True",
833
+ },
776
834
  username=self.user.username,
777
835
  )
778
- self._common_no_error_test_assertion(Role, job_result, 2, pk__in=pk_list, color="aa1409")
836
+ self._common_no_error_test_assertion(
837
+ Status, job_result, statuses.count(), name__istartswith="A", color="aa1409"
838
+ )
839
+ self.assertNotEqual(status_to_ignore.color, "aa1409")
840
+
841
+ def test_bulk_edit_objects_updating_the_same_field_as_the_filter_param(self):
842
+ """
843
+ Test bulk edit where the filter query param and the field to update are the same.
844
+ e.g.
845
+ form_data = {"status": "Test Deprecated"}
846
+ filter_params = {"status": "Test Active"}
847
+ """
848
+ self.add_permissions("ipam.change_ipaddress", "extras.view_status")
849
+ # By default Active and Available are some of the example of Status that starts with A
850
+ active_status = Status.objects.create(name="Test Active")
851
+ deprecated_status = Status.objects.create(name="Test Deprecated")
852
+ active_status.content_types.add(self.ipaddress_ct)
853
+ deprecated_status.content_types.add(self.ipaddress_ct)
854
+ IPAddress.objects.all().update(status=active_status)
855
+ self.assertEqual(IPAddress.objects.all().count(), IPAddress.objects.filter(status=active_status).count())
856
+
857
+ # Update all IPAddresses with status=active_status to deprecated_statuses with no filters
858
+ create_job_result_and_run_job(
859
+ "nautobot.core.jobs.bulk_actions",
860
+ "BulkEditObjects",
861
+ content_type=self.ipaddress_ct.id,
862
+ edit_all=True,
863
+ # pk ignored if edit_all is True
864
+ form_data={
865
+ "status": str(deprecated_status.pk),
866
+ "_all": "True",
867
+ },
868
+ username=self.user.username,
869
+ )
870
+ self.assertEqual(IPAddress.objects.all().count(), IPAddress.objects.filter(status=deprecated_status).count())
871
+ # Update all IPAddresses with status=active_status to deprecated_statuses with status filter applied
872
+ create_job_result_and_run_job(
873
+ "nautobot.core.jobs.bulk_actions",
874
+ "BulkEditObjects",
875
+ content_type=self.ipaddress_ct.id,
876
+ edit_all=True,
877
+ filter_query_params={"status": "Test Deprecated"},
878
+ # pk ignored if edit_all is True
879
+ form_data={
880
+ "status": str(active_status.pk),
881
+ "per_page": 1,
882
+ "_all": "True",
883
+ },
884
+ username=self.user.username,
885
+ )
886
+ self.assertEqual(IPAddress.objects.all().count(), IPAddress.objects.filter(status=active_status).count())
779
887
 
780
888
 
781
889
  class BulkDeleteTestCase(TransactionTestCase):
782
890
  """
783
- Test the BulkDelete system job.
891
+ Test the BulkDeleteObjects system job.
784
892
  """
785
893
 
786
894
  def setUp(self):
787
895
  super().setUp()
788
896
  self.status_ct = ContentType.objects.get_for_model(Status)
789
897
  self.role_ct = ContentType.objects.get_for_model(Role)
790
- self.device_ct = ContentType.objects.get_for_model(Device)
898
+ for x in range(10):
899
+ Status.objects.create(name=f"Example Status {x}")
900
+
901
+ statuses = Status.objects.get_for_model(Circuit)
902
+ circuit_type = CircuitType.objects.create(
903
+ name="Example Circuit Type",
904
+ )
905
+ provider = Provider.objects.create(
906
+ name="Example Provider",
907
+ )
908
+
909
+ Circuit.objects.create(
910
+ cid="Circuit 1",
911
+ provider=provider,
912
+ circuit_type=circuit_type,
913
+ status=statuses[0],
914
+ )
915
+ Circuit.objects.create(
916
+ cid="Circuit 2",
917
+ provider=provider,
918
+ circuit_type=circuit_type,
919
+ status=statuses[0],
920
+ )
921
+ Circuit.objects.create(
922
+ cid="Circuit 3",
923
+ provider=provider,
924
+ circuit_type=circuit_type,
925
+ status=statuses[0],
926
+ )
791
927
 
792
928
  def _common_no_error_test_assertion(self, model, job_result, **filter_params):
793
929
  self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
@@ -799,7 +935,7 @@ class BulkDeleteTestCase(TransactionTestCase):
799
935
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR).exists()
800
936
  )
801
937
 
802
- def test_bulk_delete_without_permission(self):
938
+ def test_bulk_delete_objects_without_permission(self):
803
939
  statuses_to_delete = [str(status) for status in Status.objects.all().values_list("pk", flat=True)[:2]]
804
940
  job_result = create_job_result_and_run_job(
805
941
  "nautobot.core.jobs.bulk_actions",
@@ -838,61 +974,66 @@ class BulkDeleteTestCase(TransactionTestCase):
838
974
  )
839
975
  self.assertEqual(Status.objects.filter(pk__in=statuses_to_delete).count(), len(statuses_to_delete))
840
976
 
841
- def test_bulk_delete_all(self):
842
- self.add_permissions("extras.delete_objectmetadata")
843
-
844
- # Assert ObjectMetadata is not empty
845
- self.assertNotEqual(ObjectMetadata.objects.all().count(), 0)
977
+ def test_bulk_delete_objects_select_all(self):
978
+ """
979
+ Delete all Circuits objects.
980
+ """
981
+ self.add_permissions("circuits.delete_circuit")
982
+ # Assert Circuit is not empty
983
+ self.assertNotEqual(Circuit.objects.all().count(), 0)
846
984
 
847
985
  job_result = create_job_result_and_run_job(
848
986
  "nautobot.core.jobs.bulk_actions",
849
987
  "BulkDeleteObjects",
850
- content_type=ContentType.objects.get_for_model(ObjectMetadata).id,
988
+ content_type=ContentType.objects.get_for_model(Circuit).id,
851
989
  delete_all=True,
852
- filter_query_params={},
990
+ filter_query_params={"per_page": 10},
853
991
  pk_list=[],
854
992
  username=self.user.username,
855
993
  )
856
- self._common_no_error_test_assertion(ObjectMetadata, job_result)
857
-
858
- def test_bulk_delete_filter_all(self):
859
- self.add_permissions("extras.delete_status")
860
- for x in range(10):
861
- Status.objects.create(name=f"Example Status {x}")
994
+ self._common_no_error_test_assertion(Circuit, job_result)
862
995
 
863
- status_to_ignore = Status.objects.create(name="Ignore Example Status")
996
+ def test_bulk_delete_objects_select_some(self):
997
+ """
998
+ Delete some of the Role objects with no filter queries applied.
999
+ """
1000
+ self.add_permissions("extras.delete_role")
1001
+ roles_to_delete = [Role.objects.create(name=f"Example Role {x}") for x in range(3)]
1002
+ roles_to_ignore = Role.objects.create(name="Ignore Example Role")
1003
+ roles_pks = [str(role.pk) for role in roles_to_delete]
864
1004
  job_result = create_job_result_and_run_job(
865
1005
  "nautobot.core.jobs.bulk_actions",
866
1006
  "BulkDeleteObjects",
867
- content_type=self.status_ct.id,
868
- delete_all=True,
869
- filter_query_params={"name__isw": "Example Status"},
1007
+ content_type=self.role_ct.id,
1008
+ delete_all=False,
1009
+ filter_query_params={},
1010
+ pk_list=roles_pks,
870
1011
  username=self.user.username,
871
1012
  )
872
- self._common_no_error_test_assertion(Status, job_result, name__istartswith="Example Status")
873
- self.assertTrue(Status.objects.filter(name=status_to_ignore.name).exists())
1013
+ self._common_no_error_test_assertion(Role, job_result, pk__in=roles_pks)
1014
+ self.assertTrue(Role.objects.filter(name=roles_to_ignore.name).exists())
874
1015
 
875
- def test_bulk_delete_passing_both_pk_list_and_delete_all(self):
1016
+ def test_bulk_delete_objects_filter_all(self):
1017
+ """
1018
+ Delete all of the Status objects that start with "Example Status".
1019
+ """
876
1020
  self.add_permissions("extras.delete_status")
877
- status_count = Status.objects.all().count()
1021
+ status_to_ignore = Status.objects.create(name="Ignore Example Status")
878
1022
  job_result = create_job_result_and_run_job(
879
1023
  "nautobot.core.jobs.bulk_actions",
880
1024
  "BulkDeleteObjects",
881
1025
  content_type=self.status_ct.id,
882
1026
  delete_all=True,
883
- filter_query_params={"name__isw": "Example Status"},
884
- pk_list=[str(Status.objects.first().pk)],
1027
+ filter_query_params={"name__isw": "Example Status", "sort": "name"},
885
1028
  username=self.user.username,
886
1029
  )
887
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
888
- job_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
889
- self.assertEqual(
890
- job_log.message,
891
- "You can either delete objs within `pk_list` provided or `delete_all` with `filter_query_params` if needed.",
892
- )
893
- self.assertEqual(status_count, Status.objects.all().count())
1030
+ self._common_no_error_test_assertion(Status, job_result, name__istartswith="Example Status")
1031
+ self.assertTrue(Status.objects.filter(name=status_to_ignore.name).exists())
894
1032
 
895
- def test_bulk_delete_with_pk(self):
1033
+ def test_bulk_delete_objects_filter_some(self):
1034
+ """
1035
+ Delete some of the Role objects that start with "Example Status".
1036
+ """
896
1037
  self.add_permissions("extras.delete_role")
897
1038
  roles_to_delete = [Role.objects.create(name=f"Example Role {x}") for x in range(3)]
898
1039
  roles_to_ignore = Role.objects.create(name="Ignore Example Role")
@@ -902,9 +1043,25 @@ class BulkDeleteTestCase(TransactionTestCase):
902
1043
  "BulkDeleteObjects",
903
1044
  content_type=self.role_ct.id,
904
1045
  delete_all=False,
905
- filter_query_params={},
1046
+ filter_query_params={"name__isw": "Example Status"},
906
1047
  pk_list=roles_pks,
907
1048
  username=self.user.username,
908
1049
  )
909
1050
  self._common_no_error_test_assertion(Role, job_result, pk__in=roles_pks)
910
1051
  self.assertTrue(Role.objects.filter(name=roles_to_ignore.name).exists())
1052
+
1053
+ def test_bulk_delete_objects_passing_both_pk_list_and_delete_all(self):
1054
+ """
1055
+ delete_all should override pk_list if both are passed.
1056
+ """
1057
+ self.add_permissions("extras.delete_status")
1058
+ job_result = create_job_result_and_run_job(
1059
+ "nautobot.core.jobs.bulk_actions",
1060
+ "BulkDeleteObjects",
1061
+ content_type=self.status_ct.pk,
1062
+ delete_all=True,
1063
+ filter_query_params={"name__isw": "Example Status", "sort": "name"},
1064
+ pk_list=[str(Status.objects.first().pk)],
1065
+ username=self.user.username,
1066
+ )
1067
+ self._common_no_error_test_assertion(Role, job_result, name__istartswith="Example Status")
@@ -72,6 +72,15 @@ class NaturalKeyTestCase(BaseModelTest):
72
72
  dt = DeviceType.objects.first()
73
73
  self.assertEqual(dt.natural_key(), [dt.manufacturer.name, dt.model])
74
74
 
75
+ def test_natural_key_with_proxy_model(self):
76
+ """Test that natural_key_field_lookups function returns the same value as its base class."""
77
+
78
+ class ProxyManufacturer(Manufacturer):
79
+ class Meta:
80
+ proxy = True
81
+
82
+ self.assertEqual(ProxyManufacturer.natural_key_field_lookups, Manufacturer.natural_key_field_lookups)
83
+
75
84
  @skip("Composite keys aren't being supported at this time")
76
85
  def test_composite_key(self):
77
86
  """Test the composite_key default implementation with some representative models."""