nautobot 2.4.20__py3-none-any.whl → 2.4.21__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.
Files changed (91) hide show
  1. nautobot/circuits/templates/circuits/circuit.html +1 -1
  2. nautobot/circuits/templates/circuits/circuittermination.html +1 -1
  3. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  4. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  5. nautobot/core/cli/migrate_deprecated_templates.py +200 -0
  6. nautobot/core/jobs/__init__.py +2 -1
  7. nautobot/core/jobs/groups.py +31 -1
  8. nautobot/core/models/tree_queries.py +10 -5
  9. nautobot/core/signals.py +12 -1
  10. nautobot/core/templates/components/panel/panel.html +1 -1
  11. nautobot/core/templates/inc/image_attachments.html +2 -1
  12. nautobot/core/templatetags/helpers.py +22 -0
  13. nautobot/core/tests/runner.py +3 -0
  14. nautobot/core/tests/test_cli.py +40 -0
  15. nautobot/core/tests/test_forms.py +41 -0
  16. nautobot/core/tests/test_jobs.py +75 -1
  17. nautobot/core/tests/test_tree_queries.py +14 -1
  18. nautobot/core/ui/object_detail.py +41 -5
  19. nautobot/core/utils/filtering.py +11 -9
  20. nautobot/core/views/generic.py +3 -3
  21. nautobot/dcim/models/device_components.py +81 -68
  22. nautobot/dcim/templates/dcim/device/config.html +1 -1
  23. nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
  24. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
  25. nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
  26. nautobot/dcim/templates/dcim/device/frontports.html +1 -1
  27. nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
  28. nautobot/dcim/templates/dcim/device/inventory.html +1 -1
  29. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  30. nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
  31. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
  32. nautobot/dcim/templates/dcim/device/powerports.html +1 -1
  33. nautobot/dcim/templates/dcim/device/rearports.html +1 -1
  34. nautobot/dcim/templates/dcim/device/status.html +1 -1
  35. nautobot/dcim/templates/dcim/device/wireless.html +1 -1
  36. nautobot/dcim/templates/dcim/device.html +1 -1
  37. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  38. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  39. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  40. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  41. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  42. nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
  43. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  44. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  45. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  46. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  47. nautobot/dcim/tests/test_models.py +43 -3
  48. nautobot/dcim/tests/test_views.py +52 -21
  49. nautobot/dcim/views.py +203 -87
  50. nautobot/extras/api/views.py +9 -1
  51. nautobot/extras/filters/customfields.py +9 -3
  52. nautobot/extras/models/groups.py +42 -5
  53. nautobot/extras/signals.py +20 -19
  54. nautobot/extras/tables.py +31 -2
  55. nautobot/extras/templates/extras/computedfield.html +1 -1
  56. nautobot/extras/templates/extras/configcontext.html +1 -1
  57. nautobot/extras/templates/extras/configcontextschema_validation.html +1 -1
  58. nautobot/extras/templates/extras/customfield.html +1 -1
  59. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  60. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  61. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
  62. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  63. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  64. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  65. nautobot/extras/templates/extras/tag.html +1 -1
  66. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
  67. nautobot/extras/tests/test_api.py +1 -0
  68. nautobot/extras/tests/test_changelog.py +28 -0
  69. nautobot/extras/tests/test_customfields.py +10 -2
  70. nautobot/extras/tests/test_dynamicgroups.py +37 -1
  71. nautobot/extras/views.py +49 -19
  72. nautobot/ipam/signals.py +71 -0
  73. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  74. nautobot/ipam/templates/ipam/service.html +1 -1
  75. nautobot/ipam/templates/ipam/vlan.html +1 -1
  76. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  77. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  78. nautobot/ipam/tests/test_models.py +42 -0
  79. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  80. nautobot/users/views.py +2 -2
  81. nautobot/virtualization/models.py +1 -68
  82. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  83. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  84. nautobot/virtualization/tests/test_models.py +42 -3
  85. {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/METADATA +9 -9
  86. {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/RECORD +90 -86
  87. nautobot-2.4.21.dist-info/entry_points.txt +4 -0
  88. nautobot-2.4.20.dist-info/entry_points.txt +0 -3
  89. {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/LICENSE.txt +0 -0
  90. {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/NOTICE +0 -0
  91. {nautobot-2.4.20.dist-info → nautobot-2.4.21.dist-info}/WHEEL +0 -0
@@ -1,243 +1,2 @@
1
1
  {% extends 'generic/object_retrieve.html' %}
2
- {% load buttons %}
3
- {% load plugins %}
4
- {% load helpers %}
5
- {% load tz %}
6
-
7
- {% block content_left_page %}
8
- <div class="panel panel-default">
9
- <div class="panel-heading">
10
- <strong>Location</strong>
11
- </div>
12
- <table class="table table-hover panel-body attr-table">
13
- <tr>
14
- <td>Location Type</td>
15
- <td>{{ object.location_type|hyperlinked_object:"name" }}</td>
16
- </tr>
17
- <tr>
18
- <td>Status</td>
19
- <td>
20
- {{ object.status| hyperlinked_object_with_color }}
21
- </td>
22
- </tr>
23
- <tr>
24
- <td>Hierarchy</td>
25
- <td>
26
- {% include 'dcim/inc/location_hierarchy.html' with location=object %}
27
- </td>
28
- </tr>
29
- {% include 'inc/tenant_table_row.html' %}
30
- <tr>
31
- <td>Facility</td>
32
- <td>{{ object.facility|placeholder }}</td>
33
- </tr>
34
- <tr>
35
- <td>AS Number</td>
36
- <td>{{ object.asn|placeholder }}</td>
37
- </tr>
38
- <tr>
39
- <td>Time Zone</td>
40
- <td>
41
- {% if object.time_zone %}
42
- {{ object.time_zone }} (UTC {{ object.time_zone|tzoffset }})<br />
43
- <small class="text-muted">Local time: {% timezone object.time_zone %}{% now "SHORT_DATETIME_FORMAT" %}{% endtimezone %}</small>
44
- {% else %}
45
- <span class="text-muted">&mdash;</span>
46
- {% endif %}
47
- </td>
48
- </tr>
49
- <tr>
50
- <td>Description</td>
51
- <td>{{ object.description|placeholder }}</td>
52
- </tr>
53
- <tr>
54
- <td>Children</td>
55
- <td>
56
- {% if object.children.all %}
57
- <a href="{% url 'dcim:location_list' %}?parent={{ object.pk }}">{{ children_table.rows|length }}</a>
58
- {% else %}
59
- {{ None|placeholder }}
60
- {% endif %}
61
- </td>
62
- </tr>
63
- </table>
64
- </div>
65
- <div class="panel panel-default">
66
- <div class="panel-heading">
67
- <strong>Geographical Info</strong>
68
- </div>
69
- <table class="table table-hover panel-body attr-table">
70
- <tr>
71
- <td>Physical Address</td>
72
- <td>
73
- {% if object.physical_address %}
74
- <div class="pull-right noprint">
75
- <a href="https://maps.google.com/?q={{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-xs">
76
- <i class="mdi mdi-map-marker"></i> Map it
77
- </a>
78
- </div>
79
- <span>{{ object.physical_address|linebreaksbr }}</span>
80
- {% else %}
81
- <span class="text-muted">&mdash;</span>
82
- {% endif %}
83
- </td>
84
- </tr>
85
- <tr>
86
- <td>Shipping Address</td>
87
- <td>{{ object.shipping_address|linebreaksbr|placeholder }}</td>
88
- </tr>
89
- <tr>
90
- <td>GPS Coordinates</td>
91
- <td>
92
- {% if object.latitude and object.longitude %}
93
- <div class="pull-right noprint">
94
- <a href="https://maps.google.com/?q={{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-xs">
95
- <i class="mdi mdi-map-marker"></i> Map it
96
- </a>
97
- </div>
98
- <span>{{ object.latitude }}, {{ object.longitude }}</span>
99
- {% else %}
100
- <span class="text-muted">&mdash;</span>
101
- {% endif %}
102
- </td>
103
- </tr>
104
- </table>
105
- </div>
106
- {% if show_convert_to_contact_button %}
107
- <div class="panel panel-default">
108
- <div class="panel-heading">
109
- <strong>Contact Info</strong>
110
- </div>
111
- <table class="table table-hover panel-body attr-table">
112
- <tr>
113
- <td>Contact Name</td>
114
- <td>{{ object.contact_name|placeholder }}</td>
115
- </tr>
116
- <tr>
117
- <td>Contact Phone</td>
118
- <td>{{ object.contact_phone|hyperlinked_phone_number }}</td>
119
- </tr>
120
- <tr>
121
- <td>Contact E-Mail</td>
122
- <td>{{ object.contact_email|hyperlinked_email }}</td>
123
- </tr>
124
- </table>
125
- {% if request.user|has_perms:contact_association_permission %}
126
- {% with request.path|add:"?tab=contacts"|urlencode as return_url %}
127
- <div class="panel-footer text-right noprint">
128
- <a href="{% url 'dcim:location_migrate_data_to_contact' pk=object.pk %}?return_url={{return_url}}" class="btn btn-primary btn-xs">
129
- <span class="mdi mdi-account-edit" aria-hidden="true"></span>
130
- Convert to contact/team record
131
- </a>
132
- </div>
133
- {% endwith %}
134
- {% endif %}
135
- </div>
136
- {% endif %}
137
- <div class="panel panel-default">
138
- <div class="panel-heading">
139
- <strong>Comments</strong>
140
- </div>
141
- <div class="panel-body rendered-markdown">
142
- {% if object.comments %}
143
- {{ object.comments|render_markdown }}
144
- {% else %}
145
- <span class="text-muted">None</span>
146
- {% endif %}
147
- </div>
148
- </div>
149
- {% endblock content_left_page %}
150
-
151
- {% block content_right_page %}
152
- <div class="panel panel-default">
153
- <div class="panel-heading">
154
- <strong>Stats</strong>
155
- </div>
156
- <div class="panel-body">
157
- <div class="col-md-4 text-center">
158
- <h2><a href="{% url 'dcim:rack_list' %}?location={{ object.pk }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
159
- <p>Racks</p>
160
- </div>
161
- <div class="col-md-4 text-center">
162
- <h2><a href="{% url 'dcim:device_list' %}?location={{ object.pk }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
163
- <p>Devices</p>
164
- </div>
165
- <div class="col-md-4 text-center">
166
- <h2><a href="{% url 'ipam:prefix_list' %}?locations={{ object.pk }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
167
- <p>Prefixes</p>
168
- </div>
169
- <div class="col-md-4 text-center">
170
- <h2><a href="{% url 'ipam:vlan_list' %}?locations={{ object.pk }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
171
- <p>VLANs</p>
172
- </div>
173
- <div class="col-md-4 text-center">
174
- <h2><a href="{% url 'circuits:circuit_list' %}?location={{ object.pk }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
175
- <p>Circuits</p>
176
- </div>
177
- <div class="col-md-4 text-center">
178
- <h2><a href="{% url 'virtualization:virtualmachine_list' %}?location={{ object.pk }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
179
- <p>Virtual Machines</p>
180
- </div>
181
- </div>
182
- </div>
183
- <div class="panel panel-default">
184
- <div class="panel-heading">
185
- <strong>Rack Groups</strong>
186
- </div>
187
- <table class="table table-hover panel-body">
188
- {% for rg in rack_groups %}
189
- <tr>
190
- <td style="padding-left: {{ rg.tree_depth }}8px"><i class="mdi mdi-folder-open"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
191
- <td>{{ rg.rack_count }}</td>
192
- <td class="text-right noprint">
193
- <a href="{% url 'dcim:rack_elevation_list' %}?rack_group={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
194
- <i class="mdi mdi-server"></i>
195
- </a>
196
- </td>
197
- </tr>
198
- {% endfor %}
199
- <tr>
200
- <td><i class="mdi mdi-folder-open"></i> All racks</td>
201
- <td>{{ stats.rack_count }}</td>
202
- <td class="text-right noprint">
203
- <a href="{% url 'dcim:rack_elevation_list' %}?location={{ object.pk }}" class="btn btn-xs btn-primary" title="View elevations">
204
- <i class="mdi mdi-server"></i>
205
- </a>
206
- </td>
207
- </tr>
208
- </table>
209
- </div>
210
- <div class="panel panel-default">
211
- <div class="panel-heading">
212
- <strong>Images</strong>
213
- </div>
214
- {% include 'inc/image_attachments.html' with images=object.images.all %}
215
- {% if perms.extras.add_imageattachment %}
216
- <div class="panel-footer text-right noprint">
217
- <a href="{% url 'dcim:location_add_image' object_id=object.pk %}" class="btn btn-primary btn-xs">
218
- <span class="mdi mdi-plus-thick" aria-hidden="true"></span>
219
- Attach an image
220
- </a>
221
- </div>
222
- {% endif %}
223
- </div>
224
- {% endblock content_right_page %}
225
-
226
- {% block content_full_width_page %}
227
- {% if object.children.all %}
228
- <div class="panel panel-default">
229
- <div class="panel-heading">
230
- <strong>Children</strong>
231
- </div>
232
- {% include 'inc/table.html' with table=children_table %}
233
- {% if perms.dcim.add_location %}
234
- <div class="panel-footer text-right noprint">
235
- <a href="{% url 'dcim:location_add' %}?parent={{ object.pk }}" class="btn btn-xs btn-primary">
236
- <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add child
237
- </a>
238
- </div>
239
- {% endif %}
240
- </div>
241
- {% include 'inc/paginator.html' with paginator=children_table.paginator page=children_table.page %}
242
- {% endif %}
243
- {% endblock content_full_width_page %}
2
+ {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/powerfeed_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/powerpanel_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/virtualchassis_retrieve.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -2834,6 +2834,11 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2834
2834
  self.assertEqual(count, 1)
2835
2835
  self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
2836
2836
 
2837
+ # add a single instance which is already there
2838
+ count = interface.add_ip_addresses(ips[-1])
2839
+ self.assertEqual(count, 0)
2840
+ self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
2841
+
2837
2842
  # add multiple instances
2838
2843
  count = interface.add_ip_addresses(ips[:5])
2839
2844
  self.assertEqual(count, 5)
@@ -2841,6 +2846,20 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2841
2846
  for ip in ips[:5]:
2842
2847
  self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
2843
2848
 
2849
+ # add multiple instances all of which are already there
2850
+ count = interface.add_ip_addresses(ips[:5])
2851
+ self.assertEqual(count, 0)
2852
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 6)
2853
+ for ip in ips[:5]:
2854
+ self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
2855
+
2856
+ # add multiple IPs some of which are there
2857
+ count = interface.add_ip_addresses(ips[3:7])
2858
+ self.assertEqual(count, 2)
2859
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 8)
2860
+ for ip in ips[3:7]:
2861
+ self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
2862
+
2844
2863
  def test_remove_ip_addresses(self):
2845
2864
  """Test the `remove_ip_addresses` helper method on `Interface`"""
2846
2865
  interface = Interface.objects.create(
@@ -2863,13 +2882,28 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2863
2882
  self.assertEqual(count, 1)
2864
2883
  self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
2865
2884
 
2885
+ # remove a single instance which has already been removed
2886
+ count = interface.remove_ip_addresses(ips[-1])
2887
+ self.assertEqual(count, 0)
2888
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
2889
+
2866
2890
  # remove multiple instances
2867
2891
  count = interface.remove_ip_addresses(ips[:5])
2868
2892
  self.assertEqual(count, 5)
2869
2893
  self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
2870
2894
 
2895
+ # remove multiple instances all which have already been removed
2896
+ count = interface.remove_ip_addresses(ips[:5])
2897
+ self.assertEqual(count, 0)
2898
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
2899
+
2900
+ # remove multiple instances some of which have already been removed
2901
+ count = interface.remove_ip_addresses(ips[3:7])
2902
+ self.assertEqual(count, 2)
2903
+ self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 2)
2904
+
2871
2905
  count = interface.remove_ip_addresses(ips)
2872
- self.assertEqual(count, 4)
2906
+ self.assertEqual(count, 2)
2873
2907
  self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 0)
2874
2908
 
2875
2909
  # Test the pre_delete signal for IPAddressToInterface instances
@@ -2877,10 +2911,16 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2877
2911
  self.device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=4).first()
2878
2912
  self.device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
2879
2913
  self.device.save()
2880
- interface.remove_ip_addresses(self.device.primary_ip4)
2914
+
2915
+ count = interface.remove_ip_addresses(self.device.primary_ip4)
2916
+ self.assertEqual(count, 1)
2881
2917
  self.device.refresh_from_db()
2882
2918
  self.assertEqual(self.device.primary_ip4, None)
2883
- interface.remove_ip_addresses(self.device.primary_ip6)
2919
+ # NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
2920
+ # NOTE: does not remove a v6 address, because there are no v6 IPs created in this test
2921
+ # NOTE: class.
2922
+ count = interface.remove_ip_addresses(self.device.primary_ip6)
2923
+ self.assertEqual(count, 0)
2884
2924
  self.device.refresh_from_db()
2885
2925
  self.assertEqual(self.device.primary_ip6, None)
2886
2926
 
@@ -7,7 +7,6 @@ from django.contrib.auth import get_user_model
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from django.db.models import Q
9
9
  from django.test import override_settings
10
- from django.test.client import RequestFactory
11
10
  from django.urls import reverse
12
11
  from django.utils.html import strip_spaces_between_tags
13
12
  from netaddr import EUI
@@ -109,7 +108,6 @@ from nautobot.dcim.models import (
109
108
  from nautobot.dcim.views import (
110
109
  ConsoleConnectionsListView,
111
110
  InterfaceConnectionsListView,
112
- LocationUIViewSet,
113
111
  PowerConnectionsListView,
114
112
  )
115
113
  from nautobot.extras.choices import CustomFieldTypeChoices, RelationshipTypeChoices
@@ -211,8 +209,10 @@ class LocationTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewT
211
209
 
212
210
  class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
213
211
  model = Location
214
- # One query for the natural slug, one for `LocationViewSet.get_extra_context`
215
- allowed_number_of_tree_queries_per_view_type = {"retrieve": 2}
212
+ # One query for the natural slug, one for `LocationViewSet.get_extra_context`, and additional for distinct (which may be fixable?)
213
+ # See: https://github.com/nautobot/nautobot/pull/7530#discussion_r2432836062
214
+ # and https://github.com/nautobot/nautobot/pull/7530#discussion_r2432620239 for additional context
215
+ allowed_number_of_tree_queries_per_view_type = {"retrieve": 3}
216
216
 
217
217
  @classmethod
218
218
  def setUpTestData(cls):
@@ -442,7 +442,6 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
442
442
  self.assertEqual(location.contact_email, "")
443
443
 
444
444
  def test_get_extra_context(self):
445
- view_set = LocationUIViewSet()
446
445
  child_1, child_2 = Location.objects.filter(name__startswith="Leaf ")
447
446
  parent_location = child_1.parent
448
447
  child_1.location_type.content_types.add(ContentType.objects.get_for_model(Prefix))
@@ -451,12 +450,24 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
451
450
  prefix_2 = Prefix.objects.create(network="192.0.2.128", prefix_length=25, status=status)
452
451
  prefix_1.locations.set([child_1, child_2])
453
452
  prefix_2.locations.set([child_1, child_2])
454
- request = RequestFactory().get(parent_location.get_absolute_url())
455
- request.user = self.user
453
+
456
454
  self.add_permissions("dcim.view_location")
457
455
  self.add_permissions("ipam.view_prefix")
458
- context = view_set.get_extra_context(request=request, instance=parent_location)
459
- self.assertEqual(context["stats"]["prefix_count"], 2)
456
+
457
+ url = parent_location.get_absolute_url()
458
+ context = self.client.get(url).context
459
+ prefix_count = (
460
+ Prefix.objects.filter(location__in=parent_location.descendants(include_self=True)).distinct().count()
461
+ )
462
+
463
+ # Ensure that the context contains "stats" and the expected stat for prefix_list
464
+ self.assertIn("stats", context)
465
+ found = False
466
+ for stat in context["stats"].values():
467
+ if stat[0] == "ipam:prefix_list":
468
+ self.assertEqual(stat[1], prefix_count)
469
+ found = True
470
+ self.assertTrue(found, "ipam:prefix_list stat not found in context['stats']")
460
471
 
461
472
 
462
473
  class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases.BulkEditObjectsViewTestCase):
@@ -2185,7 +2196,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2185
2196
  )
2186
2197
  cls.custom_fields[0].content_types.set([ContentType.objects.get_for_model(Device)])
2187
2198
 
2188
- devices = (
2199
+ cls.devices = (
2189
2200
  Device.objects.create(
2190
2201
  name="Device 1",
2191
2202
  location=locations[0],
@@ -2250,16 +2261,40 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2250
2261
  intf_status = Status.objects.get_for_model(Interface).first()
2251
2262
  intf_role = Role.objects.get_for_model(Interface).first()
2252
2263
  cls.interfaces = (
2253
- Interface.objects.create(device=devices[0], name="Interface A1", status=intf_status, role=intf_role),
2254
- Interface.objects.create(device=devices[0], name="Interface A2", status=intf_status),
2255
- Interface.objects.create(device=devices[0], name="Interface A3", status=intf_status, role=intf_role),
2264
+ Interface.objects.create(device=cls.devices[0], name="Interface A1", status=intf_status, role=intf_role),
2265
+ Interface.objects.create(device=cls.devices[0], name="Interface A2", status=intf_status),
2266
+ Interface.objects.create(device=cls.devices[0], name="Interface A3", status=intf_status, role=intf_role),
2256
2267
  )
2257
2268
 
2258
- for device, ipaddress in zip(devices, ipaddresses):
2269
+ for device, ipaddress in zip(cls.devices, ipaddresses):
2259
2270
  RelationshipAssociation(
2260
2271
  relationship=cls.relationships[0], source=device, destination=ipaddress
2261
2272
  ).validated_save()
2262
2273
 
2274
+ powerports = (
2275
+ PowerPort.objects.create(device=cls.devices[0], name="Power Port 1"),
2276
+ PowerPort.objects.create(device=cls.devices[0], name="Power Port 2"),
2277
+ PowerPort.objects.create(device=cls.devices[0], name="Power Port 3"),
2278
+ )
2279
+ PowerOutlet.objects.create(device=cls.devices[0], name="Power Outlet A")
2280
+
2281
+ powerpanel = PowerPanel.objects.create(location=locations[0], name="Power Panel 1")
2282
+ pf_status = Status.objects.get_for_model(PowerFeed).first()
2283
+ powerfeed = PowerFeed.objects.create(
2284
+ power_panel=powerpanel,
2285
+ name="Power Feed 1",
2286
+ status=pf_status,
2287
+ available_power=1000,
2288
+ phase=PowerFeedPhaseChoices.PHASE_3PHASE,
2289
+ )
2290
+
2291
+ status_connected = Status.objects.get(name="Connected")
2292
+
2293
+ Cable.objects.create(termination_a=powerports[0], termination_b=powerfeed, status=status_connected)
2294
+
2295
+ poweroutlet = PowerOutlet.objects.create(device=cls.devices[1], name="Power Outlet 1")
2296
+ Cable.objects.create(termination_a=powerports[1], termination_b=poweroutlet, status=status_connected)
2297
+
2263
2298
  cls.form_data = {
2264
2299
  "device_type": devicetypes[1].pk,
2265
2300
  "role": deviceroles[1].pk,
@@ -2407,11 +2442,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2407
2442
 
2408
2443
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2409
2444
  def test_device_powerports(self):
2410
- device = Device.objects.first()
2411
-
2412
- PowerPort.objects.create(device=device, name="Power Port 1")
2413
- PowerPort.objects.create(device=device, name="Power Port 2")
2414
- PowerPort.objects.create(device=device, name="Power Port 3")
2445
+ device = self.devices[0]
2415
2446
 
2416
2447
  url = reverse("dcim:device_powerports", kwargs={"pk": device.pk})
2417
2448
  self.assertHttpStatus(self.client.get(url), 200)
@@ -4075,8 +4106,8 @@ class PowerConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
4075
4106
  )
4076
4107
 
4077
4108
  poweroutlets = (
4078
- PowerOutlet.objects.create(device=device_2, name="Power Outlet 1", power_port=powerports[0]),
4079
- PowerOutlet.objects.create(device=device_2, name="Power Outlet 2", power_port=powerports[1]),
4109
+ PowerOutlet.objects.create(device=device_2, name="Power Outlet 1"),
4110
+ PowerOutlet.objects.create(device=device_2, name="Power Outlet 2"),
4080
4111
  )
4081
4112
 
4082
4113
  powerpanel = PowerPanel.objects.create(location=location, name="Power Panel 1")