nautobot 2.4.21__py3-none-any.whl → 2.4.22__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 (54) hide show
  1. nautobot/apps/choices.py +4 -0
  2. nautobot/apps/utils.py +8 -0
  3. nautobot/circuits/views.py +6 -2
  4. nautobot/core/cli/migrate_deprecated_templates.py +28 -9
  5. nautobot/core/filters.py +4 -0
  6. nautobot/core/forms/__init__.py +2 -0
  7. nautobot/core/forms/widgets.py +21 -2
  8. nautobot/core/settings.py +6 -0
  9. nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
  10. nautobot/core/templatetags/helpers.py +9 -7
  11. nautobot/core/tests/nautobot_config.py +3 -0
  12. nautobot/core/tests/test_templatetags_helpers.py +6 -0
  13. nautobot/core/tests/test_ui.py +49 -1
  14. nautobot/core/tests/test_utils.py +41 -1
  15. nautobot/core/ui/object_detail.py +7 -2
  16. nautobot/core/urls.py +7 -8
  17. nautobot/core/utils/filtering.py +11 -1
  18. nautobot/core/utils/lookup.py +46 -0
  19. nautobot/core/views/mixins.py +21 -16
  20. nautobot/dcim/api/serializers.py +3 -0
  21. nautobot/dcim/choices.py +49 -0
  22. nautobot/dcim/constants.py +7 -0
  23. nautobot/dcim/filters/__init__.py +7 -0
  24. nautobot/dcim/forms.py +89 -3
  25. nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
  26. nautobot/dcim/models/device_component_templates.py +33 -1
  27. nautobot/dcim/models/device_components.py +21 -0
  28. nautobot/dcim/tables/devices.py +14 -0
  29. nautobot/dcim/tables/devicetypes.py +8 -1
  30. nautobot/dcim/templates/dcim/interface.html +8 -0
  31. nautobot/dcim/templates/dcim/interface_edit.html +2 -0
  32. nautobot/dcim/tests/test_api.py +186 -6
  33. nautobot/dcim/tests/test_filters.py +32 -0
  34. nautobot/dcim/tests/test_forms.py +110 -8
  35. nautobot/dcim/tests/test_graphql.py +44 -1
  36. nautobot/dcim/tests/test_models.py +265 -0
  37. nautobot/dcim/tests/test_tables.py +160 -0
  38. nautobot/dcim/tests/test_views.py +64 -1
  39. nautobot/dcim/views.py +86 -77
  40. nautobot/extras/forms/forms.py +3 -1
  41. nautobot/extras/templates/extras/plugin_detail.html +2 -2
  42. nautobot/extras/urls.py +0 -14
  43. nautobot/extras/views.py +1 -1
  44. nautobot/ipam/ui.py +0 -17
  45. nautobot/ipam/views.py +2 -2
  46. nautobot/project-static/js/forms.js +92 -14
  47. nautobot/virtualization/tests/test_models.py +4 -2
  48. nautobot/virtualization/views.py +1 -0
  49. {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/METADATA +4 -4
  50. {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/RECORD +54 -51
  51. {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/LICENSE.txt +0 -0
  52. {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/NOTICE +0 -0
  53. {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/WHEEL +0 -0
  54. {nautobot-2.4.21.dist-info → nautobot-2.4.22.dist-info}/entry_points.txt +0 -0
nautobot/dcim/views.py CHANGED
@@ -485,25 +485,31 @@ class LocationUIViewSet(NautobotUIViewSet):
485
485
  )
486
486
 
487
487
  def get_extra_context(self, request, instance):
488
- if instance is None:
489
- return super().get_extra_context(request, instance)
488
+ context = super().get_extra_context(request, instance)
490
489
 
491
- # This query can get really expensive when there are big location trees in the DB. By casting it to a list we
492
- # ensure it is only performed once rather than as a subquery for each of the different count stats.
493
- related_locations = list(
494
- instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
495
- )
490
+ if self.action == "retrieve":
491
+ # This query can get really expensive when there are big location trees in the DB. By casting it to a list we
492
+ # ensure it is only performed once rather than as a subquery for each of the different count stats.
493
+ related_locations = list(
494
+ instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
495
+ )
496
496
 
497
- rack_groups = (
498
- RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
499
- .restrict(request.user, "view")
500
- .filter(location__in=related_locations)
501
- )
497
+ rack_groups = (
498
+ RackGroup.objects.annotate(rack_count=count_related(Rack, "rack_group"))
499
+ .restrict(request.user, "view")
500
+ .filter(location__in=related_locations)
501
+ )
502
502
 
503
- return {
504
- "rack_groups": rack_groups,
505
- "rack_count": Rack.objects.restrict(request.user, "view").filter(location__in=related_locations).count(),
506
- }
503
+ context.update(
504
+ {
505
+ "rack_groups": rack_groups,
506
+ "rack_count": Rack.objects.restrict(request.user, "view")
507
+ .filter(location__in=related_locations)
508
+ .count(),
509
+ }
510
+ )
511
+
512
+ return context
507
513
 
508
514
 
509
515
  class MigrateLocationDataToContactView(generic.ObjectEditView):
@@ -1480,65 +1486,68 @@ class ModuleTypeUIViewSet(
1480
1486
  return super().get_required_permission()
1481
1487
 
1482
1488
  def get_extra_context(self, request, instance):
1483
- if not instance:
1484
- return {}
1489
+ context = super().get_extra_context(request, instance)
1490
+ if self.action == "retrieve":
1491
+ instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
1485
1492
 
1486
- instance_count = Module.objects.restrict(request.user).filter(module_type=instance).count()
1493
+ # Component tables
1494
+ consoleport_table = tables.ConsolePortTemplateTable(
1495
+ ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1496
+ orderable=False,
1497
+ )
1498
+ consoleserverport_table = tables.ConsoleServerPortTemplateTable(
1499
+ ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1500
+ orderable=False,
1501
+ )
1502
+ powerport_table = tables.PowerPortTemplateTable(
1503
+ PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1504
+ orderable=False,
1505
+ )
1506
+ poweroutlet_table = tables.PowerOutletTemplateTable(
1507
+ PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1508
+ orderable=False,
1509
+ )
1510
+ interface_table = tables.InterfaceTemplateTable(
1511
+ list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
1512
+ orderable=False,
1513
+ )
1514
+ front_port_table = tables.FrontPortTemplateTable(
1515
+ FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1516
+ orderable=False,
1517
+ )
1518
+ rear_port_table = tables.RearPortTemplateTable(
1519
+ RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1520
+ orderable=False,
1521
+ )
1522
+ modulebay_table = tables.ModuleBayTemplateTable(
1523
+ ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1524
+ orderable=False,
1525
+ )
1526
+ if request.user.has_perm("dcim.change_moduletype"):
1527
+ consoleport_table.columns.show("pk")
1528
+ consoleserverport_table.columns.show("pk")
1529
+ powerport_table.columns.show("pk")
1530
+ poweroutlet_table.columns.show("pk")
1531
+ interface_table.columns.show("pk")
1532
+ front_port_table.columns.show("pk")
1533
+ rear_port_table.columns.show("pk")
1534
+ modulebay_table.columns.show("pk")
1487
1535
 
1488
- # Component tables
1489
- consoleport_table = tables.ConsolePortTemplateTable(
1490
- ConsolePortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1491
- orderable=False,
1492
- )
1493
- consoleserverport_table = tables.ConsoleServerPortTemplateTable(
1494
- ConsoleServerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1495
- orderable=False,
1496
- )
1497
- powerport_table = tables.PowerPortTemplateTable(
1498
- PowerPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1499
- orderable=False,
1500
- )
1501
- poweroutlet_table = tables.PowerOutletTemplateTable(
1502
- PowerOutletTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1503
- orderable=False,
1504
- )
1505
- interface_table = tables.InterfaceTemplateTable(
1506
- list(InterfaceTemplate.objects.restrict(request.user, "view").filter(module_type=instance)),
1507
- orderable=False,
1508
- )
1509
- front_port_table = tables.FrontPortTemplateTable(
1510
- FrontPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1511
- orderable=False,
1512
- )
1513
- rear_port_table = tables.RearPortTemplateTable(
1514
- RearPortTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1515
- orderable=False,
1516
- )
1517
- modulebay_table = tables.ModuleBayTemplateTable(
1518
- ModuleBayTemplate.objects.restrict(request.user, "view").filter(module_type=instance),
1519
- orderable=False,
1520
- )
1521
- if request.user.has_perm("dcim.change_moduletype"):
1522
- consoleport_table.columns.show("pk")
1523
- consoleserverport_table.columns.show("pk")
1524
- powerport_table.columns.show("pk")
1525
- poweroutlet_table.columns.show("pk")
1526
- interface_table.columns.show("pk")
1527
- front_port_table.columns.show("pk")
1528
- rear_port_table.columns.show("pk")
1529
- modulebay_table.columns.show("pk")
1536
+ context.update(
1537
+ {
1538
+ "instance_count": instance_count,
1539
+ "consoleport_table": consoleport_table,
1540
+ "consoleserverport_table": consoleserverport_table,
1541
+ "powerport_table": powerport_table,
1542
+ "poweroutlet_table": poweroutlet_table,
1543
+ "interface_table": interface_table,
1544
+ "front_port_table": front_port_table,
1545
+ "rear_port_table": rear_port_table,
1546
+ "modulebay_table": modulebay_table,
1547
+ }
1548
+ )
1530
1549
 
1531
- return {
1532
- "instance_count": instance_count,
1533
- "consoleport_table": consoleport_table,
1534
- "consoleserverport_table": consoleserverport_table,
1535
- "powerport_table": powerport_table,
1536
- "poweroutlet_table": poweroutlet_table,
1537
- "interface_table": interface_table,
1538
- "front_port_table": front_port_table,
1539
- "rear_port_table": rear_port_table,
1540
- "modulebay_table": modulebay_table,
1541
- }
1550
+ return context
1542
1551
 
1543
1552
  @action(
1544
1553
  detail=False,
@@ -2302,7 +2311,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
2302
2311
 
2303
2312
  def get_queryset(self):
2304
2313
  queryset = super().get_queryset()
2305
- if self.detail: # TODO: change to self.action == "retrieve" as a part of addressing NAUTOBOT-1051
2314
+ if self.action == "retrieve":
2306
2315
  queryset = queryset.select_related(
2307
2316
  "cluster__cluster_group",
2308
2317
  "controller_managed_device_group__controller",
@@ -3062,7 +3071,7 @@ class DeviceUIViewSet(NautobotUIViewSet):
3062
3071
  def get_extra_context(self, request, instance):
3063
3072
  extra_context = super().get_extra_context(request, instance)
3064
3073
 
3065
- if self.detail: # TODO: change to `if self.action == "retrieve"` as a part of addressing NAUTOBOT-1051
3074
+ if self.action == "retrieve":
3066
3075
  # VirtualChassis members
3067
3076
  if instance.virtual_chassis is not None:
3068
3077
  vc_members = (
@@ -5816,7 +5825,7 @@ class ControllerUIViewSet(NautobotUIViewSet):
5816
5825
  object_detail.DistinctViewTab(
5817
5826
  weight=700,
5818
5827
  tab_id="wireless_networks",
5819
- url_name="dcim:controller_wirelessnetworks",
5828
+ url_name="dcim:controller_wireless_networks",
5820
5829
  label="Wireless Networks",
5821
5830
  related_object_attribute="wireless_network_assignments",
5822
5831
  panels=(
@@ -5840,12 +5849,12 @@ class ControllerUIViewSet(NautobotUIViewSet):
5840
5849
  @action(
5841
5850
  detail=True,
5842
5851
  url_path="wireless-networks",
5843
- url_name="wirelessnetworks",
5852
+ url_name="wireless_networks",
5844
5853
  methods=["get"],
5845
5854
  custom_view_base_action="view",
5846
5855
  custom_view_additional_permissions=["wireless.view_controllermanageddevicegroupwirelessnetworkassignment"],
5847
5856
  )
5848
- def wirelessnetworks(self, request, *args, **kwargs):
5857
+ def wireless_networks(self, request, *args, **kwargs):
5849
5858
  return Response({})
5850
5859
 
5851
5860
 
@@ -386,7 +386,9 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
386
386
  role = DynamicModelMultipleChoiceField(
387
387
  queryset=Role.objects.get_for_models([Device, VirtualMachine]), to_field_name="name", required=False
388
388
  )
389
- type = DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all(), to_field_name="model", required=False)
389
+ device_type = DynamicModelMultipleChoiceField(
390
+ queryset=DeviceType.objects.all(), to_field_name="model", required=False
391
+ )
390
392
  platform = DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), to_field_name="name", required=False)
391
393
  cluster_group = DynamicModelMultipleChoiceField(
392
394
  queryset=ClusterGroup.objects.all(), to_field_name="name", required=False
@@ -83,11 +83,11 @@
83
83
  <table class="table table-hover panel-body attr-table">
84
84
  <tr>
85
85
  <td>Min Nautobot Version</td>
86
- <td>v{{ object.min_version | placeholder }}</td>
86
+ <td>{% if object.min_version %}v{{ object.min_version }}{% else %}{{ None|placeholder }}{% endif %}</td>
87
87
  </tr>
88
88
  <tr>
89
89
  <td>Max Nautobot Version</td>
90
- <td>v{{ object.max_version | placeholder }}</td>
90
+ <td>{% if object.max_version %}v{{ object.max_version }}{% else %}{{ None|placeholder }}{% endif %}</td>
91
91
  </tr>
92
92
  </table>
93
93
  </div>
nautobot/extras/urls.py CHANGED
@@ -4,7 +4,6 @@ from nautobot.core.views.routers import NautobotUIViewSetRouter
4
4
  from nautobot.extras import views
5
5
  from nautobot.extras.models import (
6
6
  Job,
7
- Relationship,
8
7
  )
9
8
 
10
9
  app_name = "extras"
@@ -111,19 +110,6 @@ urlpatterns = [
111
110
  path("jobs/<str:class_path>/run/", views.JobRunView.as_view(), name="job_run_by_class_path"),
112
111
  path("jobs/edit/", views.JobBulkEditView.as_view(), name="job_bulk_edit"),
113
112
  path("jobs/delete/", views.JobBulkDeleteView.as_view(), name="job_bulk_delete"),
114
- # Custom relationships
115
- path(
116
- "relationships/<uuid:pk>/changelog/",
117
- views.ObjectChangeLogView.as_view(),
118
- name="relationship_changelog",
119
- kwargs={"model": Relationship},
120
- ),
121
- path(
122
- "relationships/<uuid:pk>/notes/",
123
- views.ObjectNotesView.as_view(),
124
- name="relationship_notes",
125
- kwargs={"model": Relationship},
126
- ),
127
113
  # Secrets
128
114
  path(
129
115
  "secrets/provider/<str:provider_slug>/form/",
nautobot/extras/views.py CHANGED
@@ -1194,7 +1194,7 @@ class GitRepositoryUIViewSet(NautobotUIViewSet):
1194
1194
  context = {
1195
1195
  **super().get_extra_context(request, instance),
1196
1196
  "result": job_result or {},
1197
- "base_template": "extras/configcontextschema_retrieve.html",
1197
+ "base_template": "extras/gitrepository_retrieve.html",
1198
1198
  "object": instance,
1199
1199
  "active_tab": "result",
1200
1200
  "verbose_name": instance._meta.verbose_name,
nautobot/ipam/ui.py CHANGED
@@ -10,29 +10,12 @@ from nautobot.core.ui.object_detail import (
10
10
  Button,
11
11
  KeyValueTablePanel,
12
12
  ObjectFieldsPanel,
13
- ObjectsTablePanel,
14
13
  )
15
14
  from nautobot.core.views.utils import get_obj_from_context
16
15
 
17
16
  logger = logging.getLogger(__name__)
18
17
 
19
18
 
20
- # TODO: can be removed as a part of NAUTOBOT-1051
21
- class PrefixChildTablePanel(ObjectsTablePanel):
22
- def should_render(self, context: Context):
23
- if not super().should_render(context):
24
- return False
25
- return context.get("active_tab") == "prefixes"
26
-
27
-
28
- # TODO: can be removed as a part of NAUTOBOT-1051
29
- class IPAddressTablePanel(ObjectsTablePanel):
30
- def should_render(self, context: Context):
31
- if not super().should_render(context):
32
- return False
33
- return context.get("active_tab") == "ip-addresses"
34
-
35
-
36
19
  class AddChildPrefixButton(Button):
37
20
  """Custom button to add a child prefix inside a Prefix detail view."""
38
21
 
nautobot/ipam/views.py CHANGED
@@ -432,7 +432,7 @@ class PrefixUIViewSet(NautobotUIViewSet):
432
432
  related_object_attribute="default_descendants",
433
433
  url_name="ipam:prefix_prefixes",
434
434
  panels=(
435
- ui.PrefixChildTablePanel(
435
+ object_detail.ObjectsTablePanel(
436
436
  section=SectionChoices.FULL_WIDTH,
437
437
  weight=100,
438
438
  context_table_key="prefix_table",
@@ -449,7 +449,7 @@ class PrefixUIViewSet(NautobotUIViewSet):
449
449
  related_object_attribute="all_ips",
450
450
  url_name="ipam:prefix_ipaddresses",
451
451
  panels=[
452
- ui.IPAddressTablePanel(
452
+ object_detail.ObjectsTablePanel(
453
453
  section=SectionChoices.FULL_WIDTH,
454
454
  weight=100,
455
455
  context_table_key="ip_table",
@@ -223,6 +223,19 @@ function initializeFormActionClick(context){
223
223
  function initializeBulkEditNullification(context){
224
224
  this_context = $(context);
225
225
  this_context.find('input:checkbox[name=_nullify]').click(function() {
226
+ var $field = $('#id_' + this.value);
227
+
228
+ // If this is a NumberWithSelect (input-group + caret menu), don't hide the
229
+ // field. Some other fields (e.g.Interface: LAG, Bridge) currently do nothing
230
+ // when _nullify is checked, so this is consistent.
231
+ var $group = $field.closest('.input-group');
232
+ var isNumberWithSelect = $group.length &&
233
+ $group.find('.input-group-btn .dropdown-menu a.set_value').length > 0;
234
+ if (isNumberWithSelect) {
235
+ return; // no UI change; _nullify still submitted
236
+ }
237
+
238
+ // Existing behavior for other fields
226
239
  $('#id_' + this.value).toggle('disabled');
227
240
  });
228
241
  }
@@ -590,19 +603,57 @@ function initializeVLANModeSelection(context){
590
603
 
591
604
  function initializeMultiValueChar(context, dropdownParent=null){
592
605
  this_context = $(context);
593
- this_context.find('.nautobot-select2-multi-value-char').select2({
594
- allowClear: true,
595
- tags: true,
596
- theme: "bootstrap",
597
- placeholder: "---------",
598
- multiple: true,
599
- dropdownParent: dropdownParent,
600
- width: "off",
601
- "language": {
602
- "noResults": function(){
603
- return "Type something to add it as an option";
604
- }
605
- },
606
+ this_context.find('.nautobot-select2-multi-value-char').each(function(){
607
+ var $el = $(this);
608
+ $el.select2({
609
+ allowClear: true,
610
+ tags: true,
611
+ theme: "bootstrap",
612
+ placeholder: "---------",
613
+ multiple: true,
614
+ dropdownParent: dropdownParent,
615
+ width: "off",
616
+ tokenSeparators: [',', ' '],
617
+ "language": {
618
+ "noResults": function(){
619
+ return "Type something to add it as an option";
620
+ }
621
+ },
622
+ });
623
+
624
+ // Ensure pressing Enter in the Select2 search adds the current token instead of submitting the form
625
+ $el.on('select2:open', function(){
626
+ const container = document.querySelector('.select2-container--open');
627
+ if (!container) return;
628
+ const search = container.querySelector('input.select2-search__field');
629
+ if (!search) return;
630
+
631
+ // Avoid stacking multiple handlers
632
+ if (search.getAttribute('data-enter-binds')) return;
633
+ search.setAttribute('data-enter-binds', '1');
634
+
635
+ search.addEventListener('keydown', function(e){
636
+ if (e.key === 'Enter'){
637
+ e.preventDefault();
638
+ e.stopPropagation();
639
+ const val = this.value.trim();
640
+ if (!val) return;
641
+ const sel = $el.get(0);
642
+ // If option doesn't exist, create it; otherwise select it
643
+ let found = Array.prototype.find.call(sel.options, function(opt){ return String(opt.value) === String(val); });
644
+ if (!found) {
645
+ sel.add(new Option(val, val, true, true));
646
+ } else {
647
+ found.selected = true;
648
+ }
649
+ // Clear the search box and notify Select2
650
+ this.value = '';
651
+ $($el).trigger('change');
652
+ // Close the dropdown so it doesn't linger after add
653
+ try { $el.select2('close'); } catch (e) {}
654
+ }
655
+ });
656
+ });
606
657
  });
607
658
  }
608
659
 
@@ -714,7 +765,34 @@ function initializeDynamicFilterForm(context){
714
765
  lookup_type_value = $(this).find(".lookup_type-select").val();
715
766
  lookup_value = $(this).find(".lookup_value-input");
716
767
  lookup_value.attr("name", lookup_type_value);
717
- })
768
+ });
769
+
770
+ // Pre-populate filter selects (default + advanced) from current URL query params,
771
+ // including free-form values that are not part of the preset choices.
772
+ (function prepopulateFilterSelectsFromURL(){
773
+ const urlParams = new URLSearchParams(window.location.search);
774
+ // Only target Select2 tag controls inside the filter UI (default sidebar and advanced modal).
775
+ // Avoids touching unrelated Select2 tagging fields elsewhere on the page (e.g., tags inputs).
776
+ const selector = '#default-filter form select.nautobot-select2-multi-value-char, #advanced-filter select.nautobot-select2-multi-value-char';
777
+ this_context.find(selector).each(function(){
778
+ const sel = this;
779
+ const name = sel.getAttribute('name');
780
+ if (!name) { return; }
781
+ const values = urlParams.getAll(name);
782
+ if (!values.length) { return; }
783
+ values.forEach(function(v){
784
+ let found = Array.prototype.find.call(sel.options, function(opt){ return String(opt.value) === String(v); });
785
+ if (!found) {
786
+ sel.add(new Option(v, v, true, true));
787
+ } else {
788
+ found.selected = true;
789
+ }
790
+ });
791
+ if (window.jQuery && $(sel).data('select2')) {
792
+ $(sel).trigger('change');
793
+ }
794
+ });
795
+ })();
718
796
 
719
797
  // Remove applied filters
720
798
  this_context.find(".remove-filter-param").on("click", function(){
@@ -131,6 +131,7 @@ class VMInterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase
131
131
  name="Int1", virtual_machine=self.virtualmachine, status=self.int_status
132
132
  )
133
133
  ips = list(IPAddress.objects.all()[:10])
134
+ self.assertEqual(len(ips), 10)
134
135
 
135
136
  # baseline (no vm_interface to ip address relationships exists)
136
137
  self.assertFalse(IPAddressToInterface.objects.filter(vm_interface=vm_interface).exists())
@@ -171,7 +172,8 @@ class VMInterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase
171
172
  vm_interface = VMInterface.objects.create(
172
173
  name="Int1", virtual_machine=self.virtualmachine, status=self.int_status
173
174
  )
174
- ips = list(IPAddress.objects.all()[:10])
175
+ ips = list(IPAddress.objects.filter(ip_version=4)[:10])
176
+ self.assertEqual(len(ips), 10)
175
177
 
176
178
  # baseline (no vm_interface to ip address relationships exists)
177
179
  self.assertFalse(IPAddressToInterface.objects.filter(vm_interface=vm_interface).exists())
@@ -219,7 +221,7 @@ class VMInterfaceTestCase(TestCase): # TODO: change to BaseModelTestCase
219
221
  self.virtualmachine.refresh_from_db()
220
222
  self.assertEqual(self.virtualmachine.primary_ip4, None)
221
223
  # NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
222
- # NOTE: does not remove a v6 address, because there are no v6 IPs created in this test
224
+ # NOTE: does not remove a v6 address, because there are no v6 IPs used in this test
223
225
  # NOTE: class.
224
226
  count = vm_interface.remove_ip_addresses(self.virtualmachine.primary_ip6)
225
227
  self.assertEqual(count, 0)
@@ -379,6 +379,7 @@ class VirtualMachineUIViewSet(NautobotUIViewSet):
379
379
  detail=True,
380
380
  url_path="config-context",
381
381
  url_name="configcontext",
382
+ custom_view_base_action="view",
382
383
  custom_view_additional_permissions=["extras.view_configcontext"],
383
384
  )
384
385
  def config_context(self, request, pk):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nautobot
3
- Version: 2.4.21
3
+ Version: 2.4.22
4
4
  Summary: Source of truth and network automation platform.
5
5
  License: Apache-2.0
6
6
  Keywords: Nautobot
@@ -20,7 +20,7 @@ Provides-Extra: mysql
20
20
  Provides-Extra: napalm
21
21
  Provides-Extra: remote-storage
22
22
  Provides-Extra: sso
23
- Requires-Dist: Django (>=4.2.25,<4.3.0)
23
+ Requires-Dist: Django (>=4.2.26,<4.3.0)
24
24
  Requires-Dist: GitPython (>=3.1.45,<3.2.0)
25
25
  Requires-Dist: Jinja2 (>=3.1.6,<3.2.0)
26
26
  Requires-Dist: Markdown (>=3.8.2,<3.9.0)
@@ -32,7 +32,7 @@ Requires-Dist: django-ajax-tables (>=1.1.1,<1.2.0)
32
32
  Requires-Dist: django-auth-ldap (>=5.2.0,<5.3.0) ; extra == "all" or extra == "ldap"
33
33
  Requires-Dist: django-celery-beat (>=2.7.0,<2.8.0)
34
34
  Requires-Dist: django-celery-results (>=2.6.0,<2.7.0)
35
- Requires-Dist: django-constance (>=4.3.2,<4.4.0)
35
+ Requires-Dist: django-constance (>=4.3.3,<4.4.0)
36
36
  Requires-Dist: django-cors-headers (>=4.9.0,<4.10.0)
37
37
  Requires-Dist: django-db-file-storage (>=0.5.6.1,<0.6.0.0)
38
38
  Requires-Dist: django-extensions (>=4.1,<4.2)
@@ -60,7 +60,7 @@ Requires-Dist: mysqlclient (>=2.2.7,<2.3.0) ; extra == "all" or extra == "mysql"
60
60
  Requires-Dist: napalm (>=4.1.0,<6.0.0) ; extra == "all" or extra == "napalm"
61
61
  Requires-Dist: netaddr (>=1.3.0,<1.4.0)
62
62
  Requires-Dist: netutils (>=1.14.0,<2.0.0)
63
- Requires-Dist: nh3 (>=0.3.1,<0.4.0)
63
+ Requires-Dist: nh3 (>=0.3.2,<0.4.0)
64
64
  Requires-Dist: packaging (>=23.1)
65
65
  Requires-Dist: prometheus-client (>=0.23.1,<0.24.0)
66
66
  Requires-Dist: psycopg2-binary (>=2.9.11,<2.10.0)