mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
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:
|
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.4.6
|
placeholder: v3.4.7
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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.4.6
|
placeholder: v3.4.7
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -129,7 +129,8 @@ social-auth-core
|
|||||||
|
|
||||||
# Django app for social-auth-core
|
# Django app for social-auth-core
|
||||||
# https://github.com/python-social-auth/social-app-django
|
# https://github.com/python-social-auth/social-app-django
|
||||||
social-auth-app-django
|
# See https://github.com/python-social-auth/social-app-django/issues/429
|
||||||
|
social-auth-app-django==5.0.0
|
||||||
|
|
||||||
# SVG image rendering (used for rack elevations)
|
# SVG image rendering (used for rack elevations)
|
||||||
# https://github.com/mozman/svgwrite
|
# https://github.com/mozman/svgwrite
|
||||||
|
@ -16,7 +16,7 @@ If true, NetBox will automatically create local accounts for users authenticated
|
|||||||
|
|
||||||
Default: `'netbox.authentication.RemoteUserBackend'`
|
Default: `'netbox.authentication.RemoteUserBackend'`
|
||||||
|
|
||||||
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins.
|
This is the Python path to the custom [Django authentication backend](https://docs.djangoproject.com/en/stable/topics/auth/customizing/) to use for external user authentication. NetBox provides two built-in backends (listed below), though custom authentication backends may also be provided by other packages or plugins. Provide a string for a single backend, or an iterable for multiple backends, which will be attempted in the order given.
|
||||||
|
|
||||||
* `netbox.authentication.RemoteUserBackend`
|
* `netbox.authentication.RemoteUserBackend`
|
||||||
* `netbox.authentication.LDAPBackend`
|
* `netbox.authentication.LDAPBackend`
|
||||||
|
@ -38,7 +38,7 @@ In order to send email, NetBox needs an email server configured. The following i
|
|||||||
* `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally)
|
* `SERVER` - Hostname or IP address of the email server (use `localhost` if running locally)
|
||||||
* `PORT` - TCP port to use for the connection (default: `25`)
|
* `PORT` - TCP port to use for the connection (default: `25`)
|
||||||
* `USERNAME` - Username with which to authenticate
|
* `USERNAME` - Username with which to authenticate
|
||||||
* `PASSSWORD` - Password with which to authenticate
|
* `PASSWORD` - Password with which to authenticate
|
||||||
* `USE_SSL` - Use SSL when connecting to the server (default: `False`)
|
* `USE_SSL` - Use SSL when connecting to the server (default: `False`)
|
||||||
* `USE_TLS` - Use TLS when connecting to the server (default: `False`)
|
* `USE_TLS` - Use TLS when connecting to the server (default: `False`)
|
||||||
* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)
|
* `SSL_CERTFILE` - Path to the PEM-formatted SSL certificate file (optional)
|
||||||
|
@ -1,15 +1,31 @@
|
|||||||
# NetBox v3.4
|
# NetBox v3.4
|
||||||
|
|
||||||
## v3.4.7 (FUTURE)
|
## v3.4.8 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.4.7 (2023-03-28)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#11645](https://github.com/netbox-community/netbox/issues/11645) - Automatically set the scheduled time when executing reports/scripts at a recurring interval
|
||||||
* [#11833](https://github.com/netbox-community/netbox/issues/11833) - Add fieldset support for custom script forms
|
* [#11833](https://github.com/netbox-community/netbox/issues/11833) - Add fieldset support for custom script forms
|
||||||
|
* [#11973](https://github.com/netbox-community/netbox/issues/11833) - Use SSID for representing wireless links, if set
|
||||||
|
* [#11977](https://github.com/netbox-community/netbox/issues/11977) - Support designating multiple backends via `REMOTE_AUTH_BACKEND` config parameter
|
||||||
|
* [#11990](https://github.com/netbox-community/netbox/issues/11990) - Improve error reporting for duplicate CSV column headings
|
||||||
|
* [#11991](https://github.com/netbox-community/netbox/issues/11991) - Enable VDC assignment during bulk import/edit of interfaces
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#11914](https://github.com/netbox-community/netbox/issues/11914) - Include parameters when exporting saved filters
|
||||||
|
* [#11933](https://github.com/netbox-community/netbox/issues/11933) - Fix cloning of saved filters
|
||||||
* [#11984](https://github.com/netbox-community/netbox/issues/11984) - Remove erroneous 802.3az PoE type
|
* [#11984](https://github.com/netbox-community/netbox/issues/11984) - Remove erroneous 802.3az PoE type
|
||||||
* [#11979](https://github.com/netbox-community/netbox/issues/11979) - Correct URL for tags in route targets list
|
* [#11979](https://github.com/netbox-community/netbox/issues/11979) - Correct URL for tags in route targets list
|
||||||
|
* [#12008](https://github.com/netbox-community/netbox/issues/12008) - Enable cloning of export templates
|
||||||
|
* [#12029](https://github.com/netbox-community/netbox/issues/12029) - Restore missing description field on virtual chassis form
|
||||||
|
* [#12038](https://github.com/netbox-community/netbox/issues/12038) - Correct display of zero values for virtual chassis member priority
|
||||||
|
* [#12048](https://github.com/netbox-community/netbox/issues/12048) - Enable cloning of tags
|
||||||
|
* [#12058](https://github.com/netbox-community/netbox/issues/12058) - Enable cloning of config contexts
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1160,6 +1160,14 @@ class InterfaceBulkEditForm(
|
|||||||
},
|
},
|
||||||
label=_('LAG')
|
label=_('LAG')
|
||||||
)
|
)
|
||||||
|
vdcs = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=VirtualDeviceContext.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Virtual Device Contexts',
|
||||||
|
query_params={
|
||||||
|
'device_id': '$device',
|
||||||
|
}
|
||||||
|
)
|
||||||
speed = forms.IntegerField(
|
speed = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=SelectSpeedWidget(),
|
widget=SelectSpeedWidget(),
|
||||||
@ -1222,14 +1230,14 @@ class InterfaceBulkEditForm(
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
|
(None, ('module', 'type', 'label', 'speed', 'duplex', 'description')),
|
||||||
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
('Addressing', ('vrf', 'mac_address', 'wwn')),
|
||||||
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
|
||||||
('PoE', ('poe_mode', 'poe_type')),
|
('PoE', ('poe_mode', 'poe_type')),
|
||||||
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
('Related Interfaces', ('parent', 'bridge', 'lag')),
|
||||||
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
|
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
|
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description',
|
||||||
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||||
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
|
'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf',
|
||||||
)
|
)
|
||||||
|
@ -12,7 +12,9 @@ from extras.models import ConfigTemplate
|
|||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.forms import NetBoxModelImportForm
|
from netbox.forms import NetBoxModelImportForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
|
from utilities.forms import (
|
||||||
|
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField, CSVModelMultipleChoiceField
|
||||||
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
from wireless.choices import WirelessRoleChoices
|
from wireless.choices import WirelessRoleChoices
|
||||||
from .common import ModuleCommonForm
|
from .common import ModuleCommonForm
|
||||||
@ -691,6 +693,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Parent LAG interface')
|
help_text=_('Parent LAG interface')
|
||||||
)
|
)
|
||||||
|
vdcs = CSVModelMultipleChoiceField(
|
||||||
|
queryset=VirtualDeviceContext.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")'
|
||||||
|
)
|
||||||
type = CSVChoiceField(
|
type = CSVChoiceField(
|
||||||
choices=InterfaceTypeChoices,
|
choices=InterfaceTypeChoices,
|
||||||
help_text=_('Physical medium')
|
help_text=_('Physical medium')
|
||||||
@ -730,7 +738,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
|
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
|
||||||
'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
'mark_connected', 'mac_address', 'wwn', 'vdcs', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
|
||||||
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
|
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -746,6 +754,7 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
|
||||||
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
|
||||||
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
|
self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
|
||||||
|
self.fields['vdcs'].queryset = self.fields['vdcs'].queryset.filter(**params)
|
||||||
|
|
||||||
def clean_enabled(self):
|
def clean_enabled(self):
|
||||||
# Make sure enabled is True when it's not included in the uploaded data
|
# Make sure enabled is True when it's not included in the uploaded data
|
||||||
@ -754,6 +763,12 @@ class InterfaceImportForm(NetBoxModelImportForm):
|
|||||||
else:
|
else:
|
||||||
return self.cleaned_data['enabled']
|
return self.cleaned_data['enabled']
|
||||||
|
|
||||||
|
def clean_vdcs(self):
|
||||||
|
for vdc in self.cleaned_data['vdcs']:
|
||||||
|
if vdc.device != self.cleaned_data['device']:
|
||||||
|
raise forms.ValidationError(f"VDC {vdc} is not assigned to device {self.cleaned_data['device']}")
|
||||||
|
return self.cleaned_data['vdcs']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortImportForm(NetBoxModelImportForm):
|
class FrontPortImportForm(NetBoxModelImportForm):
|
||||||
device = CSVModelChoiceField(
|
device = CSVModelChoiceField(
|
||||||
|
@ -359,7 +359,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'domain', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
|
'name', 'domain', 'description', 'region', 'site_group', 'site', 'rack', 'members', 'initial_position', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import QueryDict
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
@ -147,11 +148,10 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, initial=None, **kwargs):
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
|
|
||||||
# Convert any parameters delivered via initial data to a dictionary
|
# Convert any parameters delivered via initial data to JSON data
|
||||||
if initial and 'parameters' in initial:
|
if initial and 'parameters' in initial:
|
||||||
if type(initial['parameters']) is str:
|
if type(initial['parameters']) is str:
|
||||||
# TODO: Make a utility function for this
|
initial['parameters'] = json.loads(initial['parameters'])
|
||||||
initial['parameters'] = dict(QueryDict(initial['parameters']).lists())
|
|
||||||
|
|
||||||
super().__init__(*args, initial=initial, **kwargs)
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
@ -277,8 +277,14 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
|
|||||||
'tenants', 'tags', 'data_source', 'data_file',
|
'tenants', 'tags', 'data_source', 'data_file',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, initial=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
# Convert data delivered via initial data to JSON data
|
||||||
|
if initial and 'data' in initial:
|
||||||
|
if type(initial['data']) is str:
|
||||||
|
initial['data'] = json.loads(initial['data'])
|
||||||
|
|
||||||
|
super().__init__(*args, initial=initial, **kwargs)
|
||||||
|
|
||||||
# Disable data field when a DataFile has been set
|
# Disable data field when a DataFile has been set
|
||||||
if self.instance.data_file:
|
if self.instance.data_file:
|
||||||
|
@ -25,12 +25,16 @@ class ReportForm(BootstrapMixin, forms.Form):
|
|||||||
help_text=_("Interval at which this report is re-run (in minutes)")
|
help_text=_("Interval at which this report is re-run (in minutes)")
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_schedule_at(self):
|
def clean(self):
|
||||||
scheduled_time = self.cleaned_data['schedule_at']
|
scheduled_time = self.cleaned_data['schedule_at']
|
||||||
if scheduled_time and scheduled_time < timezone.now():
|
if scheduled_time and scheduled_time < local_now():
|
||||||
raise forms.ValidationError(_('Scheduled time must be in the future.'))
|
raise forms.ValidationError(_('Scheduled time must be in the future.'))
|
||||||
|
|
||||||
return scheduled_time
|
# When interval is used without schedule at, raise an exception
|
||||||
|
if self.cleaned_data['interval'] and not scheduled_time:
|
||||||
|
self.cleaned_data['schedule_at'] = local_now()
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -52,7 +52,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
# When interval is used without schedule at, raise an exception
|
# When interval is used without schedule at, raise an exception
|
||||||
if self.cleaned_data['_interval'] and not scheduled_time:
|
if self.cleaned_data['_interval'] and not scheduled_time:
|
||||||
raise forms.ValidationError(_('Scheduled time must be set when recurs is used.'))
|
self.cleaned_data['_schedule_at'] = local_now()
|
||||||
|
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ from django.conf import settings
|
|||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from jinja2.loaders import BaseLoader
|
from jinja2.loaders import BaseLoader
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
@ -10,7 +9,7 @@ from jinja2.sandbox import SandboxedEnvironment
|
|||||||
from extras.querysets import ConfigContextQuerySet
|
from extras.querysets import ConfigContextQuerySet
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import ExportTemplatesMixin, SyncedDataMixin, TagsMixin
|
from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
|
||||||
from utilities.jinja2 import ConfigTemplateLoader
|
from utilities.jinja2 import ConfigTemplateLoader
|
||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ __all__ = (
|
|||||||
# Config contexts
|
# Config contexts
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContext(SyncedDataMixin, ChangeLoggedModel):
|
class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
|
||||||
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
|
||||||
@ -114,6 +113,12 @@ class ConfigContext(SyncedDataMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
objects = ConfigContextQuerySet.as_manager()
|
objects = ConfigContextQuerySet.as_manager()
|
||||||
|
|
||||||
|
clone_fields = (
|
||||||
|
'weight', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'device_types',
|
||||||
|
'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
|
||||||
|
'tenants', 'tags', 'data',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight', 'name']
|
ordering = ['weight', 'name']
|
||||||
|
|
||||||
|
@ -250,7 +250,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -285,7 +285,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||||
content_types = models.ManyToManyField(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
related_name='export_templates',
|
related_name='export_templates',
|
||||||
@ -318,6 +318,10 @@ class ExportTemplate(SyncedDataMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_("Download file as attachment")
|
help_text=_("Download file as attachment")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clone_fields = (
|
||||||
|
'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
|
||||||
@ -417,7 +421,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
parameters = models.JSONField()
|
parameters = models.JSONField()
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'enabled', 'weight',
|
'content_types', 'weight', 'enabled', 'parameters',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -5,7 +5,7 @@ from django.utils.text import slugify
|
|||||||
from taggit.models import TagBase, GenericTaggedItemBase
|
from taggit.models import TagBase, GenericTaggedItemBase
|
||||||
|
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import ExportTemplatesMixin
|
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ __all__ = (
|
|||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
|
||||||
class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase):
|
class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
|
||||||
id = models.BigAutoField(
|
id = models.BigAutoField(
|
||||||
primary_key=True
|
primary_key=True
|
||||||
)
|
)
|
||||||
@ -31,6 +31,10 @@ class Tag(ExportTemplatesMixin, ChangeLoggedModel, TagBase):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clone_fields = (
|
||||||
|
'color', 'description',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
@ -115,7 +116,11 @@ class CloningMixin(models.Model):
|
|||||||
for field_name in getattr(self, 'clone_fields', []):
|
for field_name in getattr(self, 'clone_fields', []):
|
||||||
field = self._meta.get_field(field_name)
|
field = self._meta.get_field(field_name)
|
||||||
field_value = field.value_from_object(self)
|
field_value = field.value_from_object(self)
|
||||||
if field_value not in (None, ''):
|
if field_value and isinstance(field, models.ManyToManyField):
|
||||||
|
attrs[field_name] = [v.pk for v in field_value]
|
||||||
|
elif field_value and isinstance(field, models.JSONField):
|
||||||
|
attrs[field_name] = json.dumps(field_value)
|
||||||
|
elif field_value not in (None, ''):
|
||||||
attrs[field_name] = field_value
|
attrs[field_name] = field_value
|
||||||
|
|
||||||
# Include tags (if applicable)
|
# Include tags (if applicable)
|
||||||
|
@ -394,8 +394,10 @@ TEMPLATES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Set up authentication backends
|
# Set up authentication backends
|
||||||
|
if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
|
||||||
|
REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
REMOTE_AUTH_BACKEND,
|
*REMOTE_AUTH_BACKEND,
|
||||||
'netbox.authentication.ObjectPermissionBackend',
|
'netbox.authentication.ObjectPermissionBackend',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@
|
|||||||
{% if object.virtual_chassis.master == vc_member %}<i class="mdi mdi-check-bold"></i>{% endif %}
|
{% if object.virtual_chassis.master == vc_member %}<i class="mdi mdi-check-bold"></i>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ vc_member.vc_priority|default:"" }}
|
{{ vc_member.vc_priority|placeholder }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.domain %}
|
{% render_field form.domain %}
|
||||||
|
{% render_field form.description %}
|
||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ boto3==1.26.91
|
|||||||
Django==4.1.7
|
Django==4.1.7
|
||||||
django-cors-headers==3.14.0
|
django-cors-headers==3.14.0
|
||||||
django-debug-toolbar==3.8.1
|
django-debug-toolbar==3.8.1
|
||||||
django-filter==22.1
|
django-filter==23.1
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.14
|
django-mptt==0.14
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
@ -21,18 +21,18 @@ graphene-django==3.0.0
|
|||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==9.1.2
|
mkdocs-material==9.1.4
|
||||||
mkdocstrings[python-legacy]==0.20.0
|
mkdocstrings[python-legacy]==0.20.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.4.0
|
Pillow==9.4.0
|
||||||
psycopg2-binary==2.9.5
|
psycopg2-binary==2.9.5
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.16.0
|
sentry-sdk==1.18.0
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core[openidconnect]==4.3.0
|
social-auth-core[openidconnect]==4.4.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.3.0
|
tablib==3.4.0
|
||||||
tzdata==2022.7
|
tzdata==2023.2
|
||||||
|
|
||||||
# Workaround for #7401
|
# Workaround for #7401
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
|
Reference in New Issue
Block a user