From d3e2a6c82c000a120ba1fa6d62156297df436b0e Mon Sep 17 00:00:00 2001 From: Luke Anderson Date: Mon, 15 Feb 2021 21:01:55 +1030 Subject: [PATCH 01/90] Fix #5819 and #5872 - Fix Primary IP Sorting Issues for Devices and VMs --- netbox/dcim/tables/devices.py | 18 +++++++++++++----- netbox/virtualization/tables.py | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index edd9e7a43..52f4449af 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -1,5 +1,6 @@ import django_tables2 as tables from django_tables2.utils import Accessor +from django.conf import settings from dcim.models import ( ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, Platform, @@ -127,11 +128,18 @@ class DeviceTable(BaseTable): verbose_name='Type', text=lambda record: record.device_type.display_name ) - primary_ip = tables.Column( - linkify=True, - order_by=('primary_ip6', 'primary_ip4'), - verbose_name='IP Address' - ) + if settings.PREFER_IPV4: + primary_ip = tables.Column( + linkify=True, + order_by=('primary_ip4', 'primary_ip6'), + verbose_name='IP Address' + ) + else: + primary_ip = tables.Column( + linkify=True, + order_by=('primary_ip6', 'primary_ip4'), + verbose_name='IP Address' + ) primary_ip4 = tables.Column( linkify=True, verbose_name='IPv4 Address' diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 34a070623..e183765f1 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -1,5 +1,5 @@ import django_tables2 as tables - +from django.conf import settings from dcim.tables.devices import BaseInterfaceTable from tenancy.tables import COL_TENANT from utilities.tables import ( @@ -125,10 +125,18 @@ class VirtualMachineDetailTable(VirtualMachineTable): linkify=True, verbose_name='IPv6 Address' ) - primary_ip = tables.Column( - linkify=True, - verbose_name='IP Address' - ) + if settings.PREFER_IPV4: + primary_ip = tables.Column( + linkify=True, + order_by=('primary_ip4', 'primary_ip6'), + verbose_name='IP Address' + ) + else: + primary_ip = tables.Column( + linkify=True, + order_by=('primary_ip6', 'primary_ip4'), + verbose_name='IP Address' + ) tags = TagColumn( url_name='virtualization:virtualmachine_list' ) From 1cdd187e922a3bef6108facdac43aabbc7862696 Mon Sep 17 00:00:00 2001 From: Julian Jacobi Date: Mon, 1 Mar 2021 09:27:21 +0100 Subject: [PATCH 02/90] add custom links to device components --- netbox/dcim/models/device_components.py | 18 +++++++++--------- netbox/templates/dcim/device_component.html | 4 ++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 452aacb56..40063234f 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -198,7 +198,7 @@ class PathEndpoint(models.Model): # Console ports # -@extras_features('export_templates', 'webhooks') +@extras_features('export_templates', 'webhooks', 'custom_links') class ConsolePort(CableTermination, PathEndpoint, ComponentModel): """ A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. @@ -234,7 +234,7 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel): # Console server ports # -@extras_features('webhooks') +@extras_features('webhooks', 'custom_links') class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel): """ A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. @@ -270,7 +270,7 @@ class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel): # Power ports # -@extras_features('export_templates', 'webhooks') +@extras_features('export_templates', 'webhooks', 'custom_links') class PowerPort(CableTermination, PathEndpoint, ComponentModel): """ A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. @@ -379,7 +379,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): # Power outlets # -@extras_features('webhooks') +@extras_features('webhooks', 'custom_links') class PowerOutlet(CableTermination, PathEndpoint, ComponentModel): """ A physical power outlet (output) within a Device which provides power to a PowerPort. @@ -479,7 +479,7 @@ class BaseInterface(models.Model): return super().save(*args, **kwargs) -@extras_features('export_templates', 'webhooks') +@extras_features('export_templates', 'webhooks', 'custom_links') class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): """ A network interface within a Device. A physical Interface can connect to exactly one other Interface. @@ -624,7 +624,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): # Pass-through ports # -@extras_features('webhooks') +@extras_features('webhooks', 'custom_links') class FrontPort(CableTermination, ComponentModel): """ A pass-through port on the front of a Device. @@ -687,7 +687,7 @@ class FrontPort(CableTermination, ComponentModel): }) -@extras_features('webhooks') +@extras_features('webhooks', 'custom_links') class RearPort(CableTermination, ComponentModel): """ A pass-through port on the rear of a Device. @@ -740,7 +740,7 @@ class RearPort(CableTermination, ComponentModel): # Device bays # -@extras_features('webhooks') +@extras_features('webhooks', 'custom_links') class DeviceBay(ComponentModel): """ An empty space within a Device which can house a child device @@ -800,7 +800,7 @@ class DeviceBay(ComponentModel): # Inventory items # -@extras_features('export_templates', 'webhooks') +@extras_features('export_templates', 'webhooks', 'custom_links') class InventoryItem(MPTTModel, ComponentModel): """ An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. diff --git a/netbox/templates/dcim/device_component.html b/netbox/templates/dcim/device_component.html index c0ce453b3..234d10fdd 100644 --- a/netbox/templates/dcim/device_component.html +++ b/netbox/templates/dcim/device_component.html @@ -1,6 +1,7 @@ {% extends 'base.html' %} {% load helpers %} {% load perms %} +{% load custom_links %} {% load plugins %} {% block header %} @@ -30,6 +31,9 @@ {% endif %}

{% block title %}{{ object.device }} / {{ object }}{% endblock %}

+
+ {% custom_links object %} +
{% endblock %} From 720f05976fae150707e6146373db93bbff3e258e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 16 Mar 2021 15:57:23 -0400 Subject: [PATCH 84/90] Add JournalEntry list view w/filtering --- netbox/extras/filters.py | 30 ++++++++++------ netbox/extras/forms.py | 34 +++++++++++++++++++ netbox/extras/migrations/0058_journalentry.py | 1 + netbox/extras/models/models.py | 1 + netbox/extras/tables.py | 25 ++++++++++++++ netbox/extras/urls.py | 1 + netbox/extras/views.py | 8 +++++ netbox/templates/extras/object_journal.html | 1 + netbox/templates/inc/nav_menu.html | 3 ++ 9 files changed, 94 insertions(+), 10 deletions(-) diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 7893b050f..afe3bff16 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -119,22 +119,32 @@ class ImageAttachmentFilterSet(BaseFilterSet): class JournalEntryFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + created = django_filters.DateTimeFromToRangeFilter() assigned_object_type = ContentTypeFilter() - # created_by_id = django_filters.ModelMultipleChoiceFilter( - # queryset=User.objects.all(), - # label='User (ID)', - # ) - # created_by = django_filters.ModelMultipleChoiceFilter( - # field_name='user__username', - # queryset=User.objects.all(), - # to_field_name='username', - # label='User (name)', - # ) + created_by_id = django_filters.ModelMultipleChoiceFilter( + queryset=User.objects.all(), + label='User (ID)', + ) + created_by = django_filters.ModelMultipleChoiceFilter( + field_name='created_by__username', + queryset=User.objects.all(), + to_field_name='username', + label='User (name)', + ) class Meta: model = JournalEntry fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(comments__icontains=value) + class TagFilterSet(BaseFilterSet): q = django_filters.CharFilter( diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index f6a960bd9..a126378fa 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -386,6 +386,40 @@ class JournalEntryForm(BootstrapMixin, forms.ModelForm): } +class JournalEntryFilterForm(BootstrapMixin, forms.Form): + model = JournalEntry + q = forms.CharField( + required=False, + label=_('Search') + ) + created_after = forms.DateTimeField( + required=False, + label=_('After'), + widget=DateTimePicker() + ) + created_before = forms.DateTimeField( + required=False, + label=_('Before'), + widget=DateTimePicker() + ) + created_by_id = DynamicModelMultipleChoiceField( + queryset=User.objects.all(), + required=False, + label=_('User'), + widget=APISelectMultiple( + api_url='/api/users/users/', + ) + ) + assigned_object_type_id = DynamicModelMultipleChoiceField( + queryset=ContentType.objects.all(), + required=False, + label=_('Object Type'), + widget=APISelectMultiple( + api_url='/api/extras/content-types/', + ) + ) + + # # Change logging # diff --git a/netbox/extras/migrations/0058_journalentry.py b/netbox/extras/migrations/0058_journalentry.py index a3a83cb78..014b9ad05 100644 --- a/netbox/extras/migrations/0058_journalentry.py +++ b/netbox/extras/migrations/0058_journalentry.py @@ -23,6 +23,7 @@ class Migration(migrations.Migration): ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ], options={ + 'verbose_name_plural': 'journal entries', 'ordering': ('-created',), }, ), diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 6970265e9..1bed166f8 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -405,6 +405,7 @@ class JournalEntry(BigIDModel): class Meta: ordering = ('-created',) + verbose_name_plural = 'journal entries' def __str__(self): return f"{self.created}" diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index ff3befc11..99a3a4d71 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -98,7 +98,32 @@ class ObjectChangeTable(BaseTable): fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id') +class JournalEntryTable(BaseTable): + created = tables.DateTimeColumn( + format=settings.SHORT_DATETIME_FORMAT + ) + assigned_object_type = tables.Column( + verbose_name='Object type' + ) + assigned_object = tables.Column( + linkify=True, + orderable=False, + verbose_name='Object' + ) + actions = ButtonsColumn( + model=JournalEntry, + buttons=('edit', 'delete') + ) + + class Meta(BaseTable.Meta): + model = JournalEntry + fields = ('created', 'created_by', 'assigned_object_type', 'assigned_object', 'comments', 'actions') + + class ObjectJournalTable(BaseTable): + """ + Used for displaying a set of JournalEntries within the context of a single object. + """ created = tables.DateTimeColumn( format=settings.SHORT_DATETIME_FORMAT ) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 1b28eea84..6fbfd8bf1 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -32,6 +32,7 @@ urlpatterns = [ path('image-attachments//delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), # Journal entries + path('journal-entries/', views.JournalEntryListView.as_view(), name='journalentry_list'), path('journal-entries/add/', views.JournalEntryEditView.as_view(), name='journalentry_add'), path('journal-entries//edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'), path('journal-entries//delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index b41f09af3..b0caab4a6 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -286,6 +286,14 @@ class ImageAttachmentDeleteView(generic.ObjectDeleteView): # Journal entries # +class JournalEntryListView(generic.ObjectListView): + queryset = JournalEntry.objects.all() + filterset = filters.JournalEntryFilterSet + filterset_form = forms.JournalEntryFilterForm + table = tables.JournalEntryTable + action_buttons = ('export',) + + class JournalEntryEditView(generic.ObjectEditView): queryset = JournalEntry.objects.all() model_form = forms.JournalEntryForm diff --git a/netbox/templates/extras/object_journal.html b/netbox/templates/extras/object_journal.html index c643cc5b5..5e21b0cb2 100644 --- a/netbox/templates/extras/object_journal.html +++ b/netbox/templates/extras/object_journal.html @@ -27,4 +27,5 @@ {% endif %} {% include 'panel_table.html' %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 621251ffb..4fff16141 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -520,6 +520,9 @@