diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 1c330e8a8..ad72e0735 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.1.7 + placeholder: v3.1.8 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index eea258c09..68256471c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.1.7 + placeholder: v3.1.8 validations: required: true - type: dropdown diff --git a/docs/installation/index.md b/docs/installation/index.md index accabd8cc..905add7ab 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -11,10 +11,6 @@ The following sections detail how to set up a new instance of NetBox: 5. [HTTP server](5-http-server.md) 6. [LDAP authentication](6-ldap.md) (optional) -The video below demonstrates the installation of NetBox v3.0 on Ubuntu 20.04 for your reference. - - - ## Requirements | Dependency | Minimum Version | diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index da0a34987..8cddf65ec 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,20 +1,30 @@ # NetBox v3.1 -## v3.1.8 (FUTURE) +## v3.1.9 (FUTURE) + +--- + +## v3.1.8 (2022-02-15) ### Enhancements * [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation * [#8398](https://github.com/netbox-community/netbox/issues/8398) - Embiggen configuration form fields for banner message content +* [#8556](https://github.com/netbox-community/netbox/issues/8556) - Add full username column to changelog table +* [#8620](https://github.com/netbox-community/netbox/issues/8620) - Enable tab completion for `nbshell` ### Bug Fixes * [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility +* [#8391](https://github.com/netbox-community/netbox/issues/8391) - Null date columns should return empty strings during CSV export * [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero * [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port * [#8564](https://github.com/netbox-community/netbox/issues/8564) - Fix errant table configuration key `available_columns` +* [#8577](https://github.com/netbox-community/netbox/issues/8577) - Show contact assignment counts in global search results * [#8578](https://github.com/netbox-community/netbox/issues/8578) - Object change log tables should honor user's configured preferences * [#8604](https://github.com/netbox-community/netbox/issues/8604) - Fix tag filter on config context list filter form +* [#8609](https://github.com/netbox-community/netbox/issues/8609) - Display validation error when attempting to assign VLANs to interface with no mode during bulk edit +* [#8611](https://github.com/netbox-community/netbox/issues/8611) - Fix bulk editing for certain custom link, webhook, and journal entry fields --- diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 41982480d..92496e6b0 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1114,8 +1114,14 @@ class InterfaceBulkEditForm( def clean(self): super().clean() + if not self.cleaned_data['mode']: + if self.cleaned_data['untagged_vlan']: + raise forms.ValidationError({'untagged_vlan': "Interface mode must be specified to assign VLANs"}) + elif self.cleaned_data['tagged_vlans']: + raise forms.ValidationError({'tagged_vlans': "Interface mode must be specified to assign VLANs"}) + # Untagged interfaces cannot be assigned tagged VLANs - if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']: + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']: raise forms.ValidationError({ 'mode': "An access interface cannot have tagged VLANs assigned." }) diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 67c43777b..e16f8aeac 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -4,7 +4,9 @@ from django.contrib.contenttypes.models import ContentType from extras.choices import * from extras.models import * from extras.utils import FeatureQuery -from utilities.forms import BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect +from utilities.forms import ( + add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, ContentTypeChoiceField, StaticSelect, +) __all__ = ( 'ConfigContextBulkEditForm', @@ -58,7 +60,7 @@ class CustomLinkBulkEditForm(BulkEditForm): required=False ) button_class = forms.ChoiceField( - choices=CustomLinkButtonClassChoices, + choices=add_blank_choice(CustomLinkButtonClassChoices), required=False, widget=StaticSelect() ) @@ -116,21 +118,25 @@ class WebhookBulkEditForm(BulkEditForm): widget=BulkEditNullBooleanSelect() ) http_method = forms.ChoiceField( - choices=WebhookHttpMethodChoices, - required=False + choices=add_blank_choice(WebhookHttpMethodChoices), + required=False, + label='HTTP method' ) payload_url = forms.CharField( - required=False + required=False, + label='Payload URL' ) ssl_verification = forms.NullBooleanField( required=False, - widget=BulkEditNullBooleanSelect() + widget=BulkEditNullBooleanSelect(), + label='SSL verification' ) secret = forms.CharField( required=False ) ca_file_path = forms.CharField( - required=False + required=False, + label='CA file path' ) nullable_fields = ('secret', 'conditions', 'ca_file_path') @@ -179,7 +185,7 @@ class JournalEntryBulkEditForm(BulkEditForm): widget=forms.MultipleHiddenInput ) kind = forms.ChoiceField( - choices=JournalEntryKindChoices, + choices=add_blank_choice(JournalEntryKindChoices), required=False ) comments = forms.CharField( diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py index 4c11d8821..07f943d15 100644 --- a/netbox/extras/management/commands/nbshell.py +++ b/netbox/extras/management/commands/nbshell.py @@ -70,10 +70,23 @@ class Command(BaseCommand): return namespace def handle(self, **options): + namespace = self.get_namespace() + # If Python code has been passed, execute it and exit. if options['command']: - exec(options['command'], self.get_namespace()) + exec(options['command'], namespace) return - shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace()) + # Try to enable tab-complete + try: + import readline + import rlcompleter + except ModuleNotFoundError: + pass + else: + readline.set_completer(rlcompleter.Completer(namespace).complete) + readline.parse_and_bind('tab: complete') + + # Run interactive shell + shell = code.interact(banner=BANNER_TEXT, local=namespace) return shell diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index d37c44763..c3b9aff23 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -26,6 +26,11 @@ CONFIGCONTEXT_ACTIONS = """ {% endif %} """ +OBJECTCHANGE_FULL_NAME = """ +{% load helpers %} +{{ record.user.get_full_name|placeholder }} +""" + OBJECTCHANGE_OBJECT = """ {% if record.changed_object and record.changed_object.get_absolute_url %} {{ record.object_repr }} @@ -196,6 +201,14 @@ class ObjectChangeTable(NetBoxTable): linkify=True, format=settings.SHORT_DATETIME_FORMAT ) + user_name = tables.Column( + verbose_name='Username' + ) + full_name = tables.TemplateColumn( + template_code=OBJECTCHANGE_FULL_NAME, + verbose_name='Full Name', + orderable=False + ) action = columns.ChoiceFieldColumn() changed_object_type = columns.ContentTypeColumn( verbose_name='Type' @@ -212,7 +225,7 @@ class ObjectChangeTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ObjectChange - fields = ('id', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id') + fields = ('id', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id') class ObjectJournalTable(NetBoxTable): diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index be5f12980..e29da6617 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -18,7 +18,7 @@ from ipam.filtersets import ( from ipam.models import Aggregate, ASN, IPAddress, Prefix, VLAN, VRF from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable from tenancy.filtersets import ContactFilterSet, TenantFilterSet -from tenancy.models import Contact, Tenant +from tenancy.models import Contact, Tenant, ContactAssignment from tenancy.tables import ContactTable, TenantTable from utilities.utils import count_related from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet @@ -186,7 +186,7 @@ SEARCH_TYPES = OrderedDict(( 'url': 'tenancy:tenant_list', }), ('contact', { - 'queryset': Contact.objects.prefetch_related('group', 'assignments'), + 'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')), 'filterset': ContactFilterSet, 'table': ContactTable, 'url': 'tenancy:contact_list', diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 4be88c995..ce3f0ecae 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -4,9 +4,12 @@ from typing import Optional import django_tables2 as tables from django.conf import settings from django.contrib.auth.models import AnonymousUser +from django.db.models import DateField, DateTimeField from django.template import Context, Template from django.urls import reverse +from django.utils.formats import date_format from django.utils.safestring import mark_safe +from django_tables2.columns import library from django_tables2.utils import Accessor from extras.choices import CustomFieldTypeChoices @@ -32,6 +35,42 @@ __all__ = ( ) +@library.register +class DateColumn(tables.DateColumn): + """ + Overrides the default implementation of DateColumn to better handle null values, returning a default value for + tables and null when exporting data. It is registered in the tables library to use this class instead of the + default, making this behavior consistent in all fields of type DateField. + """ + + def value(self, value): + return value + + @classmethod + def from_field(cls, field, **kwargs): + if isinstance(field, DateField): + return cls(**kwargs) + + +@library.register +class DateTimeColumn(tables.DateTimeColumn): + """ + Overrides the default implementation of DateTimeColumn to better handle null values, returning a default value for + tables and null when exporting data. It is registered in the tables library to use this class instead of the + default, making this behavior consistent in all fields of type DateTimeField. + """ + + def value(self, value): + if value: + return date_format(value, format="SHORT_DATETIME_FORMAT") + return None + + @classmethod + def from_field(cls, field, **kwargs): + if isinstance(field, DateTimeField): + return cls(**kwargs) + + class ToggleColumn(tables.CheckBoxColumn): """ Extend CheckBoxColumn to add a "toggle all" checkbox in the column header. diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index 1b18f1bd2..70fe19304 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -38,7 +38,11 @@