nautobot 2.4.3__py3-none-any.whl → 2.4.5__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 (198) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/apps/filters.py +2 -0
  3. nautobot/circuits/filters.py +1 -1
  4. nautobot/circuits/tests/test_models.py +5 -3
  5. nautobot/cloud/filters.py +3 -6
  6. nautobot/cloud/tests/test_filters.py +21 -0
  7. nautobot/core/admin.py +2 -0
  8. nautobot/core/celery/__init__.py +5 -3
  9. nautobot/core/jobs/__init__.py +5 -3
  10. nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
  11. nautobot/core/models/utils.py +6 -1
  12. nautobot/core/templates/inc/javascript.html +1 -0
  13. nautobot/core/templatetags/ui_framework.py +20 -4
  14. nautobot/core/testing/__init__.py +2 -0
  15. nautobot/core/testing/forms.py +1 -1
  16. nautobot/core/testing/mixins.py +9 -0
  17. nautobot/core/tests/test_api.py +1 -1
  18. nautobot/core/tests/test_graphql.py +3 -3
  19. nautobot/core/tests/test_jobs.py +30 -28
  20. nautobot/core/ui/object_detail.py +1 -1
  21. nautobot/dcim/api/serializers.py +36 -0
  22. nautobot/dcim/api/views.py +1 -1
  23. nautobot/dcim/elevations.py +17 -4
  24. nautobot/dcim/factory.py +9 -1
  25. nautobot/dcim/filters/__init__.py +27 -1
  26. nautobot/dcim/forms.py +13 -1
  27. nautobot/dcim/models/devices.py +11 -5
  28. nautobot/dcim/signals.py +26 -0
  29. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  30. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  31. nautobot/dcim/tests/test_api.py +176 -0
  32. nautobot/dcim/tests/test_filters.py +56 -3
  33. nautobot/dcim/tests/test_jobs.py +4 -6
  34. nautobot/dcim/tests/test_models.py +40 -0
  35. nautobot/dcim/views.py +24 -14
  36. nautobot/extras/api/mixins.py +1 -1
  37. nautobot/extras/api/views.py +2 -2
  38. nautobot/extras/choices.py +8 -3
  39. nautobot/extras/filters/__init__.py +4 -0
  40. nautobot/extras/jobs.py +181 -103
  41. nautobot/extras/management/utils.py +13 -2
  42. nautobot/extras/models/datasources.py +11 -4
  43. nautobot/extras/models/jobs.py +20 -17
  44. nautobot/extras/plugins/__init__.py +26 -1
  45. nautobot/extras/tables.py +25 -29
  46. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  47. nautobot/extras/templates/extras/objectchange.html +28 -12
  48. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  49. nautobot/extras/test_jobs/fail.py +75 -1
  50. nautobot/extras/tests/test_api.py +17 -16
  51. nautobot/extras/tests/test_datasources.py +64 -54
  52. nautobot/extras/tests/test_filters.py +2 -0
  53. nautobot/extras/tests/test_jobs.py +69 -62
  54. nautobot/extras/tests/test_models.py +1 -1
  55. nautobot/extras/tests/test_plugins.py +32 -1
  56. nautobot/extras/tests/test_relationships.py +5 -5
  57. nautobot/extras/tests/test_views.py +12 -2
  58. nautobot/extras/views.py +10 -1
  59. nautobot/ipam/api/serializers.py +7 -8
  60. nautobot/ipam/api/views.py +2 -2
  61. nautobot/ipam/factory.py +27 -8
  62. nautobot/ipam/filters.py +67 -29
  63. nautobot/ipam/formfields.py +51 -0
  64. nautobot/ipam/forms.py +28 -1
  65. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  66. nautobot/ipam/models.py +63 -5
  67. nautobot/ipam/querysets.py +6 -0
  68. nautobot/ipam/tables.py +21 -7
  69. nautobot/ipam/templates/ipam/rir.html +1 -43
  70. nautobot/ipam/tests/test_api.py +107 -66
  71. nautobot/ipam/tests/test_filters.py +145 -5
  72. nautobot/ipam/tests/test_models.py +16 -0
  73. nautobot/ipam/tests/test_views.py +15 -2
  74. nautobot/ipam/urls.py +1 -21
  75. nautobot/ipam/views.py +24 -41
  76. nautobot/project-static/css/base.css +11 -0
  77. nautobot/project-static/css/dark.css +2 -1
  78. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  79. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  80. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  81. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  82. nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  88. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  89. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  90. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  91. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  92. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  93. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  94. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  96. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  97. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  98. nautobot/project-static/docs/development/apps/index.html +2 -35
  99. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  100. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  101. nautobot/project-static/docs/development/core/best-practices.html +0 -27
  102. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
  103. nautobot/project-static/docs/development/core/getting-started.html +12 -16
  104. nautobot/project-static/docs/development/core/homepage.html +0 -3
  105. nautobot/project-static/docs/development/core/style-guide.html +0 -5
  106. nautobot/project-static/docs/development/core/templates.html +0 -3
  107. nautobot/project-static/docs/development/core/testing.html +0 -9
  108. nautobot/project-static/docs/development/jobs/index.html +30 -43
  109. nautobot/project-static/docs/objects.inv +0 -0
  110. nautobot/project-static/docs/overview/application_stack.html +0 -18
  111. nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
  112. nautobot/project-static/docs/requirements.txt +2 -2
  113. nautobot/project-static/docs/search/search_index.json +1 -1
  114. nautobot/project-static/docs/sitemap.xml +290 -290
  115. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  116. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
  117. nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
  118. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  119. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
  120. nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
  121. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  122. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  123. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  124. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  125. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  126. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  127. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  128. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  129. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  130. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  131. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  132. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  133. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  134. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  135. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  136. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  137. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  138. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  139. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  140. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  141. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  142. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  143. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  144. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  145. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  146. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  147. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  148. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  149. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  150. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  151. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  152. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  153. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  154. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  155. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  156. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  157. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  158. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  159. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
  160. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  161. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
  162. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  163. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  164. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  165. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  166. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  167. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  168. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  169. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  170. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  171. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  172. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  173. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  174. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  175. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  176. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  177. nautobot/project-static/js/editor.js +292 -0
  178. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  179. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  180. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  181. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  182. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  183. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  184. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  185. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  186. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  187. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  188. nautobot/tenancy/filters/__init__.py +3 -5
  189. nautobot/tenancy/tests/test_filters.py +10 -0
  190. nautobot/virtualization/views.py +0 -1
  191. nautobot/wireless/tables.py +9 -4
  192. nautobot/wireless/tests/test_api.py +0 -9
  193. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
  194. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
  195. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
  196. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
  197. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
  198. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
nautobot/__init__.py CHANGED
@@ -14,18 +14,34 @@ __initialized = False
14
14
 
15
15
  def add_success_logger():
16
16
  """Add a custom log level for success messages."""
17
- SUCCESS = 25
17
+ SUCCESS = 25 # between INFO and WARNING
18
18
  logging.addLevelName(SUCCESS, "SUCCESS")
19
19
 
20
- def success(self, message, *args, **kws):
20
+ def success(self, message, *args, **kwargs):
21
+ kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 # so that funcName is the caller function, not "success"
21
22
  if self.isEnabledFor(SUCCESS):
22
- self._log(SUCCESS, message, args, **kws)
23
+ self._log(SUCCESS, message, args, **kwargs)
23
24
 
24
25
  logging.Logger.success = success
25
26
  return success
26
27
 
27
28
 
29
+ def add_failure_logger():
30
+ """Add a custom log level for failure messages less severe than an ERROR."""
31
+ FAILURE = 35 # between WARNING and ERROR
32
+ logging.addLevelName(FAILURE, "FAILURE")
33
+
34
+ def failure(self, message, *args, **kwargs):
35
+ kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 # so that funcName is the caller function, not "failure"
36
+ if self.isEnabledFor(FAILURE):
37
+ self._log(FAILURE, message, args, **kwargs)
38
+
39
+ logging.Logger.failure = failure
40
+ return failure
41
+
42
+
28
43
  add_success_logger()
44
+ add_failure_logger()
29
45
  logger = logging.getLogger(__name__)
30
46
 
31
47
 
nautobot/apps/filters.py CHANGED
@@ -43,6 +43,7 @@ from nautobot.extras.filters.mixins import (
43
43
  StatusFilter,
44
44
  )
45
45
  from nautobot.extras.plugins import FilterExtension
46
+ from nautobot.ipam.filters import PrefixFilter
46
47
  from nautobot.tenancy.filters import TenancyModelFilterSetMixin
47
48
 
48
49
  __all__ = (
@@ -72,6 +73,7 @@ __all__ = (
72
73
  "NaturalKeyOrPKMultipleChoiceFilter",
73
74
  "NautobotFilterSet",
74
75
  "NumericArrayFilter",
76
+ "PrefixFilter",
75
77
  "RelatedMembershipBooleanFilter",
76
78
  "RelationshipFilter",
77
79
  "RelationshipModelFilterSetMixin",
@@ -16,7 +16,7 @@ from nautobot.dcim.filters import (
16
16
  )
17
17
  from nautobot.dcim.models import Location
18
18
  from nautobot.extras.filters import NautobotFilterSet, StatusModelFilterSetMixin
19
- from nautobot.tenancy.filters import TenancyModelFilterSetMixin
19
+ from nautobot.tenancy.filters.mixins import TenancyModelFilterSetMixin
20
20
 
21
21
  from .models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
22
22
 
@@ -17,8 +17,7 @@ class CircuitTerminationModelTestCase(ModelTestCases.BaseModelTestCase):
17
17
  provider = Provider.objects.first()
18
18
  circuit_type = CircuitType.objects.first()
19
19
 
20
- location_type_1 = LocationType.objects.get(name="Campus")
21
- location_type_1.content_types.set([])
20
+ location_type_1 = LocationType.objects.create(name="University")
22
21
  location_type_2 = LocationType.objects.get(name="Building")
23
22
  location_type_2.content_types.add(ContentType.objects.get_for_model(CircuitTermination))
24
23
  status = Status.objects.get_for_model(Circuit).first()
@@ -26,7 +25,10 @@ class CircuitTerminationModelTestCase(ModelTestCases.BaseModelTestCase):
26
25
  cid="Circuit 1", provider=provider, circuit_type=circuit_type, status=status
27
26
  )
28
27
  cls.provider_network = ProviderNetwork.objects.create(name="Provider Network 1", provider=provider)
29
- cls.location_1 = Location.objects.filter(location_type=location_type_1)[0]
28
+ location_status = Status.objects.get_for_model(Location).first()
29
+ cls.location_1 = Location.objects.create(
30
+ name="Department", location_type=location_type_1, status=location_status
31
+ )
30
32
  cls.location_2 = Location.objects.filter(location_type=location_type_2)[0]
31
33
 
32
34
  cloud_resource_type = CloudResourceType.objects.get_for_model(CloudNetwork).first()
nautobot/cloud/filters.py CHANGED
@@ -1,5 +1,3 @@
1
- import django_filters
2
-
3
1
  from nautobot.cloud import models
4
2
  from nautobot.core.filters import (
5
3
  BaseFilterSet,
@@ -11,7 +9,7 @@ from nautobot.dcim.models import Manufacturer
11
9
  from nautobot.extras.filters import NautobotFilterSet
12
10
  from nautobot.extras.models import SecretsGroup
13
11
  from nautobot.extras.utils import FeatureQuery
14
- from nautobot.ipam.models import Prefix
12
+ from nautobot.ipam.filters import PrefixFilter
15
13
 
16
14
 
17
15
  class CloudAccountFilterSet(NautobotFilterSet):
@@ -98,7 +96,7 @@ class CloudNetworkFilterSet(NautobotFilterSet):
98
96
  queryset=models.CloudNetwork.objects.all(),
99
97
  label="Parent cloud network (name or ID)",
100
98
  )
101
- prefixes = django_filters.ModelMultipleChoiceFilter(queryset=Prefix.objects.all())
99
+ prefixes = PrefixFilter()
102
100
 
103
101
  class Meta:
104
102
  model = models.CloudNetwork
@@ -117,8 +115,7 @@ class CloudNetworkPrefixAssignmentFilterSet(BaseFilterSet):
117
115
  queryset=models.CloudNetwork.objects.all(),
118
116
  label="Cloud network (name or ID)",
119
117
  )
120
- # Prefix doesn't have an appropriate natural key for NaturalKeyOrPKMultipleChoiceFilter
121
- prefix = django_filters.ModelMultipleChoiceFilter(queryset=Prefix.objects.all())
118
+ prefix = PrefixFilter()
122
119
 
123
120
  class Meta:
124
121
  model = models.CloudNetworkPrefixAssignment
@@ -65,6 +65,7 @@ class CloudNetworkTestCase(FilterTestCases.FilterTestCase):
65
65
  ("name",),
66
66
  ("parent", "parent__id"),
67
67
  ("parent", "parent__name"),
68
+ ("prefixes", "prefixes__id"),
68
69
  ]
69
70
  exclude_q_filter_predicates = [
70
71
  "parent__name",
@@ -79,6 +80,16 @@ class CloudNetworkTestCase(FilterTestCases.FilterTestCase):
79
80
  queryset = queryset.filter(children__isnull=True)
80
81
  return queryset
81
82
 
83
+ def test_prefixes_filter_by_string(self):
84
+ """Test filtering by prefix strings as an alternative to pk."""
85
+ prefix = self.queryset.filter(prefixes__isnull=False).first().prefixes.first()
86
+ params = {"prefixes": [prefix.prefix]}
87
+ self.assertQuerysetEqualAndNotEmpty(
88
+ self.filterset(params, self.queryset).qs,
89
+ self.queryset.filter(prefixes__network=prefix.network, prefixes__prefix_length=prefix.prefix_length),
90
+ ordered=False,
91
+ )
92
+
82
93
 
83
94
  class CloudNetworkPrefixAssignmentTestCase(FilterTestCases.FilterTestCase):
84
95
  queryset = models.CloudNetworkPrefixAssignment.objects.all()
@@ -89,6 +100,16 @@ class CloudNetworkPrefixAssignmentTestCase(FilterTestCases.FilterTestCase):
89
100
  ("prefix", "prefix__id"),
90
101
  ]
91
102
 
103
+ def test_prefix_filter_by_string(self):
104
+ """Test filtering by prefix strings as an alternative to pk."""
105
+ prefix = self.queryset.first().prefix
106
+ params = {"prefix": [prefix.prefix]}
107
+ self.assertQuerysetEqualAndNotEmpty(
108
+ self.filterset(params, self.queryset).qs,
109
+ self.queryset.filter(prefix__network=prefix.network, prefix__prefix_length=prefix.prefix_length),
110
+ ordered=False,
111
+ )
112
+
92
113
 
93
114
  class CloudServiceNetworkAssignmentTestCase(FilterTestCases.FilterTestCase):
94
115
  queryset = models.CloudServiceNetworkAssignment.objects.all()
nautobot/core/admin.py CHANGED
@@ -9,7 +9,9 @@ from django_celery_beat.models import (
9
9
  PeriodicTask,
10
10
  SolarSchedule,
11
11
  )
12
+ import social_django.admin # noqa: F401 # unused-import -- but this import installs the social_django admin
12
13
  from social_django.models import Association, Nonce, UserSocialAuth
14
+ import taggit.admin # noqa: F401 # unused-import -- but this import installs the taggit admin
13
15
  from taggit.models import Tag
14
16
 
15
17
  from nautobot.core.forms import BootstrapMixin
@@ -14,7 +14,7 @@ from django.utils.module_loading import import_string
14
14
  from kombu.serialization import register
15
15
  from prometheus_client import CollectorRegistry, multiprocess, start_http_server
16
16
 
17
- from nautobot import add_success_logger
17
+ from nautobot import add_failure_logger, add_success_logger
18
18
  from nautobot.core.celery.control import discard_git_repository, refresh_git_repository # noqa: F401 # unused-import
19
19
  from nautobot.core.celery.encoders import NautobotKombuJSONEncoder
20
20
  from nautobot.core.celery.log import NautobotDatabaseHandler
@@ -138,14 +138,16 @@ def add_nautobot_log_handler(logger_instance, log_format=None):
138
138
 
139
139
  @signals.after_setup_logger.connect
140
140
  def setup_nautobot_global_logging(logger, **kwargs): # pylint: disable=redefined-outer-name
141
- """Add SUCCESS log to celery global logger."""
141
+ """Add SUCCESS and FAILURE logs to celery global logger."""
142
142
  logger.success = add_success_logger()
143
+ logger.failure = add_failure_logger()
143
144
 
144
145
 
145
146
  @signals.after_setup_task_logger.connect
146
147
  def setup_nautobot_task_logging(logger, **kwargs): # pylint: disable=redefined-outer-name
147
- """Add SUCCESS log to celery task logger."""
148
+ """Add SUCCESS and FAILURE logs to celery task logger."""
148
149
  logger.success = add_success_logger()
150
+ logger.failure = add_failure_logger()
149
151
 
150
152
 
151
153
  @signals.celeryd_after_setup.connect
@@ -229,7 +229,8 @@ class ExportObjectList(Job):
229
229
  # The force_csv=True attribute is a hack, but much easier than trying to construct a valid HttpRequest
230
230
  # object from scratch that passes all implicit and explicit assumptions in Django and DRF.
231
231
  serializer = serializer_class(queryset, many=True, context={"request": None}, force_csv=True)
232
- csv_data = renderer.render(serializer.data)
232
+ # Explicitly add UTF-8 BOM to the data so that Excel will understand non-ASCII characters correctly...
233
+ csv_data = codecs.BOM_UTF8 + renderer.render(serializer.data).encode("utf-8")
233
234
  self.create_file(filename + ".csv", csv_data)
234
235
 
235
236
 
@@ -294,8 +295,9 @@ class ImportObjects(Job):
294
295
  validation_failed = True
295
296
  else:
296
297
  validation_failed = True
297
- for field, err in serializer.errors.items():
298
- self.logger.error("Row %d: `%s`: `%s`", row, field, err[0])
298
+ for field, errs in serializer.errors.items():
299
+ for err in errs:
300
+ self.logger.error("Row %d: `%s`: `%s`", row, field, err)
299
301
  return new_objs, validation_failed
300
302
 
301
303
  def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False): # pylint:disable=arguments-differ
@@ -1,5 +1,6 @@
1
1
  from typing import Optional
2
2
 
3
+ from django.apps import apps
3
4
  from django.conf import settings
4
5
  from django.core.management.base import BaseCommand
5
6
  from django.urls import get_resolver
@@ -162,6 +163,10 @@ class Command(BaseCommand):
162
163
  if model:
163
164
  app_name = model._meta.app_label
164
165
 
166
+ # Retrieve the base URL for the app to be used in the URL pattern
167
+ app_config = apps.get_app_config(app_name)
168
+ base_url = app_config.base_url if hasattr(app_config, "base_url") else app_name
169
+
165
170
  if app_name == "users" and pattern.name in ["login", "logout"]:
166
171
  # No need to test the login and logout endpoints for performance testing
167
172
  url_pattern = f"/{pattern.pattern}" # /login, /logout
@@ -199,7 +204,7 @@ class Command(BaseCommand):
199
204
  elif is_api_endpoint:
200
205
  if not is_app:
201
206
  # One of the nautobot apps: nautobot.circuits, nautobot.dcim, and etc.
202
- url_pattern = f"/api/{app_name}/{pattern.pattern}" # /api/dcim/devices/
207
+ url_pattern = f"/api/{base_url}/{pattern.pattern}" # /api/dcim/devices/
203
208
  app_name = f"{app_name}-api" # dcim-api
204
209
  view_name = f"{app_name}:{pattern.name}" # dcim-api:device-list
205
210
  else:
@@ -207,16 +212,14 @@ class Command(BaseCommand):
207
212
  view_name = (
208
213
  f"plugins-api:{api_app_name}:{pattern.name}" # plugins-api:example_app-api:examplemodel-list
209
214
  )
210
- app_name = app_name.replace("_", "-") # example_app -> example-app
211
- url_pattern = f"/api/plugins/{app_name}/{pattern.pattern}" # /api/plugins/example-app/models/
215
+ url_pattern = f"/api/plugins/{base_url}/{pattern.pattern}" # /api/plugins/example-app/models/
212
216
  else:
213
217
  if not is_app:
214
- url_pattern = f"/{app_name}/{pattern.pattern}" # /dcim/devices/
218
+ url_pattern = f"/{base_url}/{pattern.pattern}" # /dcim/devices/
215
219
  view_name = f"{app_name}:{pattern.name}" # dcim:device_list
216
220
  else:
217
221
  view_name = f"plugins:{app_name}:{pattern.name}" # plugins:example_app:examplemodel_list
218
- app_name = app_name.replace("_", "-") # example_app -> example-app
219
- url_pattern = f"/plugins/{app_name}/{pattern.pattern}" # /plugins/example-app/models/
222
+ url_pattern = f"/plugins/{base_url}/{pattern.pattern}" # /plugins/example-app/models/
220
223
 
221
224
  return url_pattern, view_name, is_api_endpoint
222
225
 
@@ -112,7 +112,12 @@ def serialize_object(obj, extra=None, exclude=None):
112
112
 
113
113
  # Include any tags. Check for tags cached on the instance; fall back to using the manager.
114
114
  if is_taggable(obj):
115
- tags = getattr(obj, "_tags", []) or obj.tags.all()
115
+ # Note that when upgrading from Nautobot 1.x to 2.0, this method may be called during data migrations,
116
+ # specifically ipam_0022 and dcim_0034, to create ObjectChange records.
117
+ # This can be problematic (see issue #6952) as the Tag records in the DB still have `created` as a `DateField`,
118
+ # but the 2.x code expects this to be a `DateTimeField` (as it will be after the upgrade completes in full).
119
+ # We "cleverly" bypass that issue by using `.only("name")` since that's the only actual Tag field we need here.
120
+ tags = getattr(obj, "_tags", []) or obj.tags.only("name")
116
121
  data["tags"] = [tag.name for tag in tags]
117
122
 
118
123
  # Append any extra data
@@ -27,6 +27,7 @@
27
27
  <script src="{% versioned_static 'js/dropdown.js' %}"
28
28
  onerror="window.location='{% url 'media_failure' %}?filename=js/dropdown.js'"></script>
29
29
  <script type="text/javascript">
30
+ var nautobot_static_url = "{% static '' %}";
30
31
  var nautobot_api_path = "{% url 'api-root' %}";
31
32
  var nautobot_csrf_token = "{{ csrf_token }}";
32
33
  var loading = $(".loading");
@@ -1,9 +1,13 @@
1
+ import logging
2
+
1
3
  from django import template
2
4
  from django.utils.html import format_html_join
3
5
 
4
6
  from nautobot.core.utils.lookup import get_view_for_model
5
7
  from nautobot.core.views.utils import get_obj_from_context
6
8
 
9
+ logger = logging.getLogger(__name__)
10
+
7
11
  register = template.Library()
8
12
 
9
13
 
@@ -26,15 +30,27 @@ def render_components(context, components):
26
30
  @register.simple_tag(takes_context=True)
27
31
  def render_detail_view_extra_buttons(context):
28
32
  """
29
- Render the "extra_buttons" if any from the base detail view associated with the context object.
33
+ Render the "extra_buttons" from the context's object_detail_content, or as fallback, from the base detail view.
30
34
 
31
35
  This makes it possible for "extra" tabs (such as Changelog and Notes, and any added by App TemplateExtensions)
32
36
  to automatically still render any `extra_buttons` defined by the base detail view, without the tab-specific views
33
37
  needing to explicitly inherit from the base view.
34
38
  """
35
- obj = get_obj_from_context(context)
36
- base_detail_view = get_view_for_model(obj)
37
- object_detail_content = getattr(base_detail_view, "object_detail_content", None)
39
+ object_detail_content = context.get("object_detail_content")
40
+ if object_detail_content is None:
41
+ obj = get_obj_from_context(context)
42
+ if obj is None:
43
+ logger.error("No 'obj' or 'object' found in the render context!")
44
+ return ""
45
+ base_detail_view = get_view_for_model(obj)
46
+ if base_detail_view is None:
47
+ logger.warning(
48
+ "Unable to identify the base detail view - check that it has a valid name, i.e. %sUIViewSet or %sView",
49
+ type(obj).__name__,
50
+ type(obj).__name__,
51
+ )
52
+ return ""
53
+ object_detail_content = getattr(base_detail_view, "object_detail_content", None)
38
54
  if object_detail_content is not None and object_detail_content.extra_buttons:
39
55
  return render_components(context, object_detail_content.extra_buttons)
40
56
  return ""
@@ -68,6 +68,8 @@ def run_job_for_testing(job, username="test-user", profile=False, **kwargs):
68
68
  username=username, defaults={"is_superuser": True, "password": "password"}
69
69
  )
70
70
  # Run the job synchronously in the current thread as if it were being executed by a worker
71
+ # TODO: in Nautobot core testing, we set `CELERY_TASK_ALWAYS_EAGER = True`, so we *could* use enqueue_job() instead,
72
+ # but switching now would be a potentially breaking change for apps...
71
73
  job_result = JobResult.execute_job(
72
74
  job_model=job,
73
75
  user=user_instance,
@@ -24,7 +24,7 @@ class FormTestCases:
24
24
  self.skipTest(f"{self.form_class.__name__}.{field_name} has no query_params")
25
25
  field_model = field_class.queryset.model
26
26
  filterset_class = get_filterset_for_model(field_model)
27
- filterset_fields = set(filterset_class.declared_filters.keys())
27
+ filterset_fields = set(filterset_class.get_filters().keys())
28
28
  invalid_query_params = query_params_fields - filterset_fields
29
29
  self.assertFalse(
30
30
  invalid_query_params,
@@ -18,6 +18,7 @@ from nautobot.core.models import fields as core_fields
18
18
  from nautobot.core.testing import utils
19
19
  from nautobot.core.utils import permissions
20
20
  from nautobot.extras import management, models as extras_models
21
+ from nautobot.extras.choices import JobResultStatusChoices
21
22
  from nautobot.users import models as users_models
22
23
 
23
24
  # Use the proper swappable User model
@@ -188,6 +189,14 @@ class NautobotTestCaseMixin:
188
189
  err_message = f"{msg}\n{err_message}"
189
190
  self.assertIn(response.status_code, expected_status, err_message)
190
191
 
192
+ def assertJobResultStatus(self, job_result, expected_status=JobResultStatusChoices.STATUS_SUCCESS):
193
+ """Assert that the given job_result has the expected_status, or print the job logs to aid in debugging."""
194
+ self.assertEqual(
195
+ job_result.status,
196
+ expected_status,
197
+ (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
198
+ )
199
+
191
200
  def assertInstanceEqual(self, instance, data, exclude=None, api=False):
192
201
  """
193
202
  Compare a model instance to a dictionary, checking that its attribute values match those specified
@@ -609,7 +609,7 @@ class WritableNestedSerializerTest(testing.APITestCase):
609
609
  dcim_models.LocationType.objects.get(name="Building"),
610
610
  ]
611
611
  for location_type in self.locations_types:
612
- location_type.content_types.set([vlan_group_ct, vlan_ct])
612
+ location_type.content_types.add(vlan_group_ct, vlan_ct)
613
613
 
614
614
  self.statuses = extras_models.Status.objects.get_for_model(dcim_models.Location)
615
615
  self.location1 = dcim_models.Location.objects.create(
@@ -8,7 +8,7 @@ from django.apps import apps
8
8
  from django.contrib.auth import get_user_model
9
9
  from django.contrib.auth.models import Group
10
10
  from django.contrib.contenttypes.models import ContentType
11
- from django.db.models import Q
11
+ from django.db.models import Count, Q
12
12
  from django.test import override_settings, TestCase
13
13
  from django.test.client import RequestFactory
14
14
  from django.urls import reverse
@@ -919,8 +919,8 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
919
919
  priority=789,
920
920
  ),
921
921
  )
922
- prefixes = Prefix.objects.all()[:2]
923
- cls.namespace = prefixes[0].namespace
922
+ cls.namespace = Namespace.objects.annotate(prefix_count=Count("prefixes")).filter(prefix_count__gt=2).first()
923
+ prefixes = Prefix.objects.filter(namespace=cls.namespace)
924
924
  vrfs = (
925
925
  VRF.objects.create(name="VRF 1", rd="65000:100", namespace=cls.namespace),
926
926
  VRF.objects.create(name="VRF 2", rd="65000:200", namespace=cls.namespace),