jeremystretch 4 лет назад
Родитель
Сommit
aa85ae89c1

+ 1 - 1
.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

+ 1 - 1
.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

+ 0 - 4
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.
-
-<iframe width="560" height="315" src="https://www.youtube.com/embed/7Fpd2-q9_28" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
-
 ## Requirements
 
 | Dependency | Minimum Version |

+ 11 - 1
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
 
 ---
 

+ 7 - 1
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."
             })

+ 14 - 8
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(

+ 15 - 2
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

+ 14 - 1
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 %}
     <a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
@@ -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):

+ 2 - 2
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',

+ 39 - 0
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.

+ 5 - 1
netbox/templates/extras/objectchange.html

@@ -38,7 +38,11 @@
                     <tr>
                         <th scope="row">User</th>
                         <td>
-                            {{ object.user|default:object.user_name }}
+                            {% if object.user.get_full_name %}
+                              {{ object.user.get_full_name }} ({{ object.user_name }})
+                            {% else %}
+                              {{ object.user_name }}
+                            {% endif %}
                         </td>
                     </tr>
                     <tr>

+ 1 - 1
netbox/utilities/utils.py

@@ -204,7 +204,7 @@ def deepmerge(original, new):
     """
     merged = OrderedDict(original)
     for key, val in new.items():
-        if key in original and isinstance(original[key], dict) and isinstance(val, dict):
+        if key in original and isinstance(original[key], dict) and val and isinstance(val, dict):
             merged[key] = deepmerge(original[key], val)
         else:
             merged[key] = val

+ 1 - 1
netbox/virtualization/forms/bulk_import.py

@@ -65,7 +65,7 @@ class ClusterCSVForm(NetBoxModelCSVForm):
 class VirtualMachineCSVForm(NetBoxModelCSVForm):
     status = CSVChoiceField(
         choices=VirtualMachineStatusChoices,
-        help_text='Operational status of device'
+        help_text='Operational status'
     )
     cluster = CSVModelChoiceField(
         queryset=Cluster.objects.all(),

+ 2 - 2
requirements.txt

@@ -20,10 +20,10 @@ gunicorn==20.1.0
 Jinja2==3.0.3
 Markdown==3.3.6
 markdown-include==0.6.0
-mkdocs-material==8.1.9
+mkdocs-material==8.1.11
 mkdocstrings==0.17.0
 netaddr==0.8.0
-Pillow==8.4.0
+Pillow==9.0.1
 psycopg2-binary==2.9.3
 PyYAML==6.0
 social-auth-app-django==5.0.0