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
@@ -532,6 +532,7 @@ class Panel(Component):
532
532
  self,
533
533
  *,
534
534
  label="",
535
+ css_class="default",
535
536
  section=SectionChoices.FULL_WIDTH,
536
537
  body_id=None,
537
538
  body_content_template_path=None,
@@ -546,6 +547,7 @@ class Panel(Component):
546
547
 
547
548
  Args:
548
549
  label (str): Label to display for this panel. Optional; if an empty string, the panel will have no label.
550
+ css_class (str): Panel variant to render as, e.g. "default", "warning", "info".
549
551
  section (str): One of the [`SectionChoices`](./ui.md#nautobot.apps.ui.SectionChoices) values, indicating the layout section this Panel belongs to.
550
552
  body_id (str): HTML element `id` to attach to the rendered body wrapper of the panel.
551
553
  body_content_template_path (str): Template path to render the content contained *within* the panel body.
@@ -557,6 +559,7 @@ class Panel(Component):
557
559
  (a `div` or `table`) as well as its contents. Generally you won't override this as a user.
558
560
  """
559
561
  self.label = label
562
+ self.css_class = css_class
560
563
  self.section = section
561
564
  self.body_id = body_id
562
565
  self.body_content_template_path = body_content_template_path
@@ -583,6 +586,7 @@ class Panel(Component):
583
586
  self.template_path,
584
587
  context,
585
588
  label=self.render_label(context),
589
+ css_class=self.css_class,
586
590
  header_extra_content=self.render_header_extra_content(context),
587
591
  body=self.render_body(context),
588
592
  footer_content=self.render_footer_content(context),
@@ -1609,7 +1613,7 @@ class StatsPanel(Panel):
1609
1613
  instance = get_obj_from_context(context)
1610
1614
  request = context["request"]
1611
1615
  if isinstance(instance, TreeModel):
1612
- self.filter_pks = (
1616
+ self.filter_pks = list(
1613
1617
  instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
1614
1618
  )
1615
1619
  else:
@@ -1625,9 +1629,10 @@ class StatsPanel(Panel):
1625
1629
  else:
1626
1630
  related_object_model_class, query = related_field, f"{self.filter_name}__in"
1627
1631
  filter_dict = {query: self.filter_pks}
1628
- related_object_count = (
1629
- related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict).count()
1630
- )
1632
+ qs = related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict)
1633
+ if len(self.filter_pks) > 1:
1634
+ qs = qs.distinct()
1635
+ related_object_count = qs.count()
1631
1636
  related_object_model_class_meta = related_object_model_class._meta
1632
1637
  related_object_list_url = validated_viewname(related_object_model_class, "list")
1633
1638
  related_object_title = bettertitle(related_object_model_class_meta.verbose_name_plural)
@@ -2034,6 +2039,36 @@ class _ObjectDetailContactsTab(Tab):
2034
2039
  )
2035
2040
 
2036
2041
 
2042
+ class DynamicGroupsTextPanel(BaseTextPanel):
2043
+ """Panel displaying a note about caching of dynamic groups."""
2044
+
2045
+ def __init__(
2046
+ self,
2047
+ *,
2048
+ weight,
2049
+ render_as=BaseTextPanel.RenderOptions.MARKDOWN,
2050
+ label="Dynamic Group caching",
2051
+ css_class="warning",
2052
+ **kwargs,
2053
+ ):
2054
+ super().__init__(weight=weight, render_as=render_as, label=label, css_class=css_class, **kwargs)
2055
+
2056
+ def get_value(self, context):
2057
+ dg_list_url = reverse("extras:dynamicgroup_list")
2058
+ job_run_url = reverse(
2059
+ "extras:job_run_by_class_path",
2060
+ kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
2061
+ )
2062
+ return (
2063
+ "Dynamic group membership is cached for performance reasons, "
2064
+ "therefore this page may not always be up-to-date.\n\n"
2065
+ "You can refresh the membership of any specific group by accessing it from the list below or from the "
2066
+ f'[Dynamic Groups list view]({dg_list_url}) and clicking the "Refresh Members" button.\n\n'
2067
+ "You can also refresh the membership of **all** groups by running the "
2068
+ f"[Refresh Dynamic Group Caches job]({job_run_url})."
2069
+ )
2070
+
2071
+
2037
2072
  @dataclass
2038
2073
  class _ObjectDetailGroupsTab(Tab):
2039
2074
  """Built-in class for a Tab displaying information about associated dynamic groups."""
@@ -2050,8 +2085,9 @@ class _ObjectDetailGroupsTab(Tab):
2050
2085
  ):
2051
2086
  if panels is None:
2052
2087
  panels = (
2088
+ DynamicGroupsTextPanel(weight=100),
2053
2089
  ObjectsTablePanel(
2054
- weight=100,
2090
+ weight=200,
2055
2091
  table_class=DynamicGroupTable,
2056
2092
  table_attribute="dynamic_groups",
2057
2093
  exclude_columns=["content_type"],
@@ -17,6 +17,15 @@ from nautobot.core.utils.lookup import get_filterset_for_model
17
17
  # e.g `name__ic` has lookup expr `ic (icontains)` while `name` has no lookup expr
18
18
  CONTAINS_LOOKUP_EXPR_RE = re.compile(r"(?<=__)\w+")
19
19
 
20
+ MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING = {
21
+ "cables": "cable_terminations",
22
+ "metadata_types": "metadata",
23
+ "object_metadata": "metadata",
24
+ "location_types": "locations",
25
+ "static_group_associations": "dynamic_groups",
26
+ "relationship_associations": "relationships",
27
+ }
28
+
20
29
 
21
30
  def build_lookup_label(field_name, _verbose_name):
22
31
  """
@@ -131,18 +140,11 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
131
140
  elif isinstance(
132
141
  field, ContentTypeMultipleChoiceFilter
133
142
  ): # While there are other objects using `ContentTypeMultipleChoiceFilter`, the case where
134
- # models that have such a filter and the `verbose_name_plural` has multiple words is ony one: "dynamic groups".
143
+ # models that have such a filter and the `verbose_name_plural` does not match, we can lookup the feature name.
135
144
  from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
136
145
 
137
146
  plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
138
-
139
- # Cable-connectable models use "cable_terminations", not "cables", as the feature name
140
- if plural_name == "cables":
141
- plural_name = "cable_terminations"
142
- elif plural_name == "metadata_types":
143
- plural_name = "metadata"
144
- elif plural_name == "object_metadata":
145
- plural_name = "metadata"
147
+ plural_name = MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING.get(plural_name, plural_name)
146
148
  try:
147
149
  form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
148
150
  except KeyError:
@@ -580,7 +580,7 @@ class ObjectDeleteView(UIComponentsMixin, GetReturnURLMixin, ObjectPermissionReq
580
580
  """
581
581
 
582
582
  queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
583
- template_name = "generic/object_delete.html"
583
+ template_name = "generic/object_destroy.html"
584
584
 
585
585
  def get_required_permission(self):
586
586
  return get_permission_for_model(self.queryset.model, "delete")
@@ -1037,7 +1037,7 @@ class BulkEditView(
1037
1037
  filterset: Optional[type[FilterSet]] = None
1038
1038
  table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
1039
1039
  form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
1040
- template_name = "generic/object_bulk_edit.html"
1040
+ template_name = "generic/object_bulk_update.html"
1041
1041
 
1042
1042
  def get_required_permission(self):
1043
1043
  return get_permission_for_model(self.queryset.model, "change")
@@ -1239,7 +1239,7 @@ class BulkDeleteView(
1239
1239
  filterset: Optional[type[FilterSet]] = None
1240
1240
  table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
1241
1241
  form: Optional[type[Form]] = None
1242
- template_name = "generic/object_bulk_delete.html"
1242
+ template_name = "generic/object_bulk_destroy.html"
1243
1243
 
1244
1244
  def get_required_permission(self):
1245
1245
  return get_permission_for_model(self.queryset.model, "delete")
@@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType
6
6
  from django.core.cache import cache
7
7
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
8
8
  from django.core.validators import MaxValueValidator, MinValueValidator
9
- from django.db import models, transaction
9
+ from django.db import models
10
10
  from django.db.models import Sum
11
11
  from django.utils.functional import classproperty
12
12
 
@@ -595,6 +595,86 @@ class BaseInterface(RelationshipModel):
595
595
 
596
596
  return super().save(*args, **kwargs)
597
597
 
598
+ def add_ip_addresses(
599
+ self,
600
+ ip_addresses,
601
+ is_source=False,
602
+ is_destination=False,
603
+ is_default=False,
604
+ is_preferred=False,
605
+ is_primary=False,
606
+ is_secondary=False,
607
+ is_standby=False,
608
+ ):
609
+ """Add one or more IPAddress instances to this interface's `ip_addresses` many-to-many relationship.
610
+
611
+ Args:
612
+ ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
613
+ is_source (bool, optional): Is source address. Defaults to False.
614
+ is_destination (bool, optional): Is destination address. Defaults to False.
615
+ is_default (bool, optional): Is default address. Defaults to False.
616
+ is_preferred (bool, optional): Is preferred address. Defaults to False.
617
+ is_primary (bool, optional): Is primary address. Defaults to False.
618
+ is_secondary (bool, optional): Is secondary address. Defaults to False.
619
+ is_standby (bool, optional): Is standby address. Defaults to False.
620
+
621
+ Returns:
622
+ Number of instances added.
623
+ """
624
+ through_defaults = {
625
+ "is_source": is_source,
626
+ "is_destination": is_destination,
627
+ "is_default": is_default,
628
+ "is_preferred": is_preferred,
629
+ "is_primary": is_primary,
630
+ "is_secondary": is_secondary,
631
+ "is_standby": is_standby,
632
+ }
633
+
634
+ if not isinstance(ip_addresses, (tuple, list)):
635
+ ip_addresses = [ip_addresses]
636
+
637
+ # This ensures that ips_to_add only contains IPs which need to be added to the interface. This ensures
638
+ # that len(ips_to_add) accurately represents the results of the action.
639
+ ips_to_add = set(ip_addresses) - set(self.ip_addresses.all())
640
+
641
+ if ips_to_add:
642
+ self.ip_addresses.add(*ips_to_add, through_defaults=through_defaults) # pylint: disable=no-member # Intf/VMIntf both have ip_addresses
643
+
644
+ return len(ips_to_add)
645
+
646
+ add_ip_addresses.alters_data = True
647
+
648
+ def remove_ip_addresses(self, ip_addresses):
649
+ """Remove one or more IPAddress instances from this interface's `ip_addresses` many-to-many relationship.
650
+
651
+ Args:
652
+ ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
653
+
654
+ Returns:
655
+ Number of instances removed.
656
+ """
657
+ if not isinstance(ip_addresses, (tuple, list)):
658
+ ip_addresses = [ip_addresses]
659
+
660
+ # The delete() call used previously (ref: https://github.com/nautobot/nautobot/issues/3236)
661
+ # meant that if None was passed in, it was silently ignored. Rather that raise an exception,
662
+ # this comprehension maintains backwards compatibility.
663
+ ip_addresses = {ip for ip in ip_addresses if ip is not None}
664
+
665
+ # This checks that the IPs passed in are actually on the interface. By populating
666
+ # ips_to_remove correctly, we ensure that the only IPs passed to remove() are IPs known
667
+ # to be on the interface. This ensures that len(ips_to_remove) accurately represents
668
+ # the results of the action.
669
+ ips_to_remove = ip_addresses & set(self.ip_addresses.all())
670
+
671
+ if ips_to_remove:
672
+ self.ip_addresses.remove(*ips_to_remove) # pylint: disable=no-member # Intf/VMIntf both have ip_addresses
673
+
674
+ return len(ips_to_remove)
675
+
676
+ remove_ip_addresses.alters_data = True
677
+
598
678
 
599
679
  @extras_features(
600
680
  "cable_terminations",
@@ -797,73 +877,6 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
797
877
  }
798
878
  )
799
879
 
800
- def add_ip_addresses(
801
- self,
802
- ip_addresses,
803
- is_source=False,
804
- is_destination=False,
805
- is_default=False,
806
- is_preferred=False,
807
- is_primary=False,
808
- is_secondary=False,
809
- is_standby=False,
810
- ):
811
- """Add one or more IPAddress instances to this interface's `ip_addresses` many-to-many relationship.
812
-
813
- Args:
814
- ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
815
- is_source (bool, optional): Is source address. Defaults to False.
816
- is_destination (bool, optional): Is destination address. Defaults to False.
817
- is_default (bool, optional): Is default address. Defaults to False.
818
- is_preferred (bool, optional): Is preferred address. Defaults to False.
819
- is_primary (bool, optional): Is primary address. Defaults to False.
820
- is_secondary (bool, optional): Is secondary address. Defaults to False.
821
- is_standby (bool, optional): Is standby address. Defaults to False.
822
-
823
- Returns:
824
- Number of instances added.
825
- """
826
- if not isinstance(ip_addresses, (tuple, list)):
827
- ip_addresses = [ip_addresses]
828
- with transaction.atomic():
829
- for ip in ip_addresses:
830
- instance = self.ip_addresses.through(
831
- ip_address=ip,
832
- interface=self,
833
- is_source=is_source,
834
- is_destination=is_destination,
835
- is_default=is_default,
836
- is_preferred=is_preferred,
837
- is_primary=is_primary,
838
- is_secondary=is_secondary,
839
- is_standby=is_standby,
840
- )
841
- instance.validated_save()
842
- return len(ip_addresses)
843
-
844
- add_ip_addresses.alters_data = True
845
-
846
- def remove_ip_addresses(self, ip_addresses):
847
- """Remove one or more IPAddress instances from this interface's `ip_addresses` many-to-many relationship.
848
-
849
- Args:
850
- ip_addresses (:obj:`list` or `IPAddress`): Instance of `nautobot.ipam.models.IPAddress` or list of `IPAddress` instances.
851
-
852
- Returns:
853
- Number of instances removed.
854
- """
855
- count = 0
856
- if not isinstance(ip_addresses, (tuple, list)):
857
- ip_addresses = [ip_addresses]
858
- with transaction.atomic():
859
- for ip in ip_addresses:
860
- qs = self.ip_addresses.through.objects.filter(ip_address=ip, interface=self)
861
- deleted_count, _ = qs.delete()
862
- count += deleted_count
863
- return count
864
-
865
- remove_ip_addresses.alters_data = True
866
-
867
880
  @property
868
881
  def is_connectable(self):
869
882
  return self.type not in NONCONNECTABLE_IFACE_TYPES
@@ -1,4 +1,4 @@
1
- {% extends 'dcim/device/base.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
 
3
3
  {% block title %}{{ object }} - Config{% endblock %}
4
4
 
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/device/base.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/device/base.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/device/base.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/device/base.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/device/base.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/device/base.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,4 +1,4 @@
1
- {% extends 'dcim/device/base.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block title %}{{ object }} - LLDP Neighbors{% endblock %}
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/device/base.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/device/base.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/device/base.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/device/base.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,4 +1,4 @@
1
- {% extends 'dcim/device/base.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block title %}{{ object }} - Status{% endblock %}
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/device/base.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,4 +1,4 @@
1
- {% extends 'dcim/device/base.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block javascript %}
@@ -1,3 +1,3 @@
1
- {% extends 'generic/object_delete.html' %}
1
+ {% extends 'generic/object_destroy.html' %}
2
2
 
3
3
  {% block message_extra %}This would also delete any/all child interfaces of this interface.{% endblock %}
@@ -1,2 +1,2 @@
1
- {% extends 'dcim/devicetype_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 %}
@@ -0,0 +1,14 @@
1
+ {% load perms %}
2
+ {% load helpers %}
3
+ {% if perms.dcim.contact_association %}
4
+ {% if object.contact_name or object.contact_phone or object.contact_email %}
5
+ {% with request.path|add:"?tab=contacts"|urlencode as return_url %}
6
+ <div class="text-right noprint">
7
+ <a href="{% url 'dcim:location_migrate_data_to_contact' pk=object.pk %}?return_url={{ return_url }}" class="btn btn-primary btn-xs">
8
+ <span class="mdi mdi-account-edit" aria-hidden="true"></span>
9
+ Convert to contact/team record
10
+ </a>
11
+ </div>
12
+ {% endwith %}
13
+ {% endif %}
14
+ {% endif %}
@@ -1,3 +1,3 @@
1
- {% extends 'generic/object_bulk_delete.html' %}
1
+ {% extends 'generic/object_bulk_destroy.html' %}
2
2
 
3
3
  {% block message_extra %}This would also delete any/all child interfaces of these interfaces.{% endblock %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_bulk_delete.html' %}
1
+ {% extends 'generic/object_bulk_destroy.html' %}
2
2
 
3
3
  {% block message_extra %}
4
4
  <p class="text-center text-danger"><i class="mdi mdi-alert"></i> This will also delete all child inventory items of those listed.</p>