jeremystretch 4 سال پیش
والد
کامیت
aa85ae89c1

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
     attributes:
       label: NetBox version
       label: NetBox version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v3.1.7
+      placeholder: v3.1.8
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -14,7 +14,7 @@ body:
     attributes:
     attributes:
       label: NetBox version
       label: NetBox version
       description: What version of NetBox are you currently running?
       description: What version of NetBox are you currently running?
-      placeholder: v3.1.7
+      placeholder: v3.1.8
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - 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)
 5. [HTTP server](5-http-server.md)
 6. [LDAP authentication](6-ldap.md) (optional)
 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
 ## Requirements
 
 
 | Dependency | Minimum Version |
 | Dependency | Minimum Version |

+ 11 - 1
docs/release-notes/version-3.1.md

@@ -1,20 +1,30 @@
 # NetBox v3.1
 # NetBox v3.1
 
 
-## v3.1.8 (FUTURE)
+## v3.1.9 (FUTURE)
+
+---
+
+## v3.1.8 (2022-02-15)
 
 
 ### Enhancements
 ### Enhancements
 
 
 * [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation
 * [#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
 * [#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
 ### Bug Fixes
 
 
 * [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility
 * [#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
 * [#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
 * [#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`
 * [#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
 * [#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
 * [#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):
     def clean(self):
         super().clean()
         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
         # 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({
             raise forms.ValidationError({
                 'mode': "An access interface cannot have tagged VLANs assigned."
                 '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.choices import *
 from extras.models import *
 from extras.models import *
 from extras.utils import FeatureQuery
 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__ = (
 __all__ = (
     'ConfigContextBulkEditForm',
     'ConfigContextBulkEditForm',
@@ -58,7 +60,7 @@ class CustomLinkBulkEditForm(BulkEditForm):
         required=False
         required=False
     )
     )
     button_class = forms.ChoiceField(
     button_class = forms.ChoiceField(
-        choices=CustomLinkButtonClassChoices,
+        choices=add_blank_choice(CustomLinkButtonClassChoices),
         required=False,
         required=False,
         widget=StaticSelect()
         widget=StaticSelect()
     )
     )
@@ -116,21 +118,25 @@ class WebhookBulkEditForm(BulkEditForm):
         widget=BulkEditNullBooleanSelect()
         widget=BulkEditNullBooleanSelect()
     )
     )
     http_method = forms.ChoiceField(
     http_method = forms.ChoiceField(
-        choices=WebhookHttpMethodChoices,
-        required=False
+        choices=add_blank_choice(WebhookHttpMethodChoices),
+        required=False,
+        label='HTTP method'
     )
     )
     payload_url = forms.CharField(
     payload_url = forms.CharField(
-        required=False
+        required=False,
+        label='Payload URL'
     )
     )
     ssl_verification = forms.NullBooleanField(
     ssl_verification = forms.NullBooleanField(
         required=False,
         required=False,
-        widget=BulkEditNullBooleanSelect()
+        widget=BulkEditNullBooleanSelect(),
+        label='SSL verification'
     )
     )
     secret = forms.CharField(
     secret = forms.CharField(
         required=False
         required=False
     )
     )
     ca_file_path = forms.CharField(
     ca_file_path = forms.CharField(
-        required=False
+        required=False,
+        label='CA file path'
     )
     )
 
 
     nullable_fields = ('secret', 'conditions', 'ca_file_path')
     nullable_fields = ('secret', 'conditions', 'ca_file_path')
@@ -179,7 +185,7 @@ class JournalEntryBulkEditForm(BulkEditForm):
         widget=forms.MultipleHiddenInput
         widget=forms.MultipleHiddenInput
     )
     )
     kind = forms.ChoiceField(
     kind = forms.ChoiceField(
-        choices=JournalEntryKindChoices,
+        choices=add_blank_choice(JournalEntryKindChoices),
         required=False
         required=False
     )
     )
     comments = forms.CharField(
     comments = forms.CharField(

+ 15 - 2
netbox/extras/management/commands/nbshell.py

@@ -70,10 +70,23 @@ class Command(BaseCommand):
         return namespace
         return namespace
 
 
     def handle(self, **options):
     def handle(self, **options):
+        namespace = self.get_namespace()
+
         # If Python code has been passed, execute it and exit.
         # If Python code has been passed, execute it and exit.
         if options['command']:
         if options['command']:
-            exec(options['command'], self.get_namespace())
+            exec(options['command'], namespace)
             return
             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
         return shell

+ 14 - 1
netbox/extras/tables.py

@@ -26,6 +26,11 @@ CONFIGCONTEXT_ACTIONS = """
 {% endif %}
 {% endif %}
 """
 """
 
 
+OBJECTCHANGE_FULL_NAME = """
+{% load helpers %}
+{{ record.user.get_full_name|placeholder }}
+"""
+
 OBJECTCHANGE_OBJECT = """
 OBJECTCHANGE_OBJECT = """
 {% if record.changed_object and record.changed_object.get_absolute_url %}
 {% if record.changed_object and record.changed_object.get_absolute_url %}
     <a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
     <a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
@@ -196,6 +201,14 @@ class ObjectChangeTable(NetBoxTable):
         linkify=True,
         linkify=True,
         format=settings.SHORT_DATETIME_FORMAT
         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()
     action = columns.ChoiceFieldColumn()
     changed_object_type = columns.ContentTypeColumn(
     changed_object_type = columns.ContentTypeColumn(
         verbose_name='Type'
         verbose_name='Type'
@@ -212,7 +225,7 @@ class ObjectChangeTable(NetBoxTable):
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = ObjectChange
         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):
 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.models import Aggregate, ASN, IPAddress, Prefix, VLAN, VRF
 from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
 from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
 from tenancy.filtersets import ContactFilterSet, TenantFilterSet
 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 tenancy.tables import ContactTable, TenantTable
 from utilities.utils import count_related
 from utilities.utils import count_related
 from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
 from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
@@ -186,7 +186,7 @@ SEARCH_TYPES = OrderedDict((
         'url': 'tenancy:tenant_list',
         'url': 'tenancy:tenant_list',
     }),
     }),
     ('contact', {
     ('contact', {
-        'queryset': Contact.objects.prefetch_related('group', 'assignments'),
+        'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(assignment_count=count_related(ContactAssignment, 'contact')),
         'filterset': ContactFilterSet,
         'filterset': ContactFilterSet,
         'table': ContactTable,
         'table': ContactTable,
         'url': 'tenancy:contact_list',
         'url': 'tenancy:contact_list',

+ 39 - 0
netbox/netbox/tables/columns.py

@@ -4,9 +4,12 @@ from typing import Optional
 import django_tables2 as tables
 import django_tables2 as tables
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.models import AnonymousUser
 from django.contrib.auth.models import AnonymousUser
+from django.db.models import DateField, DateTimeField
 from django.template import Context, Template
 from django.template import Context, Template
 from django.urls import reverse
 from django.urls import reverse
+from django.utils.formats import date_format
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
+from django_tables2.columns import library
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from extras.choices import CustomFieldTypeChoices
 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):
 class ToggleColumn(tables.CheckBoxColumn):
     """
     """
     Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.
     Extend CheckBoxColumn to add a "toggle all" checkbox in the column header.

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

@@ -38,7 +38,11 @@
                     <tr>
                     <tr>
                         <th scope="row">User</th>
                         <th scope="row">User</th>
                         <td>
                         <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>
                         </td>
                     </tr>
                     </tr>
                     <tr>
                     <tr>

+ 1 - 1
netbox/utilities/utils.py

@@ -204,7 +204,7 @@ def deepmerge(original, new):
     """
     """
     merged = OrderedDict(original)
     merged = OrderedDict(original)
     for key, val in new.items():
     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)
             merged[key] = deepmerge(original[key], val)
         else:
         else:
             merged[key] = val
             merged[key] = val

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

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

+ 2 - 2
requirements.txt

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