mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge v3.1.8
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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
|
||||
|
@ -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 |
|
||||
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
@ -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."
|
||||
})
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user