mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-19 18:18:43 -06:00
Compare commits
2 Commits
7154d4ae2e
...
20044-elev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c39f2c7de5 | ||
|
|
68e995d551 |
@@ -74,7 +74,7 @@ The plugin source directory contains all the actual Python code and other resour
|
|||||||
|
|
||||||
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
|
||||||
|
|
||||||
```python title="__init__.py"
|
```python
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
|
|
||||||
class FooBarConfig(PluginConfig):
|
class FooBarConfig(PluginConfig):
|
||||||
@@ -151,7 +151,7 @@ Any additional apps must be installed within the same Python environment as NetB
|
|||||||
|
|
||||||
An example `pyproject.toml` is below:
|
An example `pyproject.toml` is below:
|
||||||
|
|
||||||
```toml title="pyproject.toml"
|
```
|
||||||
# See PEP 518 for the spec of this file
|
# See PEP 518 for the spec of this file
|
||||||
# https://www.python.org/dev/peps/pep-0518/
|
# https://www.python.org/dev/peps/pep-0518/
|
||||||
|
|
||||||
@@ -179,24 +179,11 @@ classifiers=[
|
|||||||
]
|
]
|
||||||
|
|
||||||
requires-python = ">=3.10.0"
|
requires-python = ">=3.10.0"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
|
Many of these are self-explanatory, but for more information, see the [pyproject.toml documentation](https://packaging.python.org/en/latest/specifications/pyproject-toml/).
|
||||||
|
|
||||||
## Compatibility Matrix
|
|
||||||
|
|
||||||
Consider adding a file named `COMPATIBILITY.md` to your plugin project root (alongside `pyproject.toml`). This file should contain a table listing the minimum and maximum supported versions of NetBox (`min_version` and `max_version`) for each release. This serves as a handy reference for users who are upgrading from a previous version of your plugin. An example is shown below:
|
|
||||||
|
|
||||||
```markdown title="COMPATIBILITY.md"
|
|
||||||
# Compatibility Matrix
|
|
||||||
|
|
||||||
| Release | Minimum NetBox Version | Maximum NetBox Version |
|
|
||||||
|---------|------------------------|------------------------|
|
|
||||||
| 0.2.0 | 4.4.0 | 4.5.x |
|
|
||||||
| 0.1.1 | 4.3.0 | 4.4.x |
|
|
||||||
| 0.1.0 | 4.3.0 | 4.4.x |
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a Virtual Environment
|
## Create a Virtual Environment
|
||||||
|
|
||||||
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like;however, it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) for the development of your plugin, as opposed to using system-wide packages. This will afford you complete control over the installed versions of all dependencies and avoid conflict with system packages. This environment can live wherever you'd like;however, it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.)
|
||||||
|
|||||||
@@ -131,19 +131,6 @@ class DataSource(JobsMixin, PrimaryModel):
|
|||||||
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
|
|
||||||
# If recurring sync is disabled for an existing DataSource, clear any pending sync jobs for it and reset its
|
|
||||||
# "queued" status
|
|
||||||
if not self._state.adding and not self.sync_interval:
|
|
||||||
self.jobs.filter(status=JobStatusChoices.STATUS_PENDING).delete()
|
|
||||||
if self.status == DataSourceStatusChoices.QUEUED and self.last_synced:
|
|
||||||
self.status = DataSourceStatusChoices.COMPLETED
|
|
||||||
elif self.status == DataSourceStatusChoices.QUEUED:
|
|
||||||
self.status = DataSourceStatusChoices.NEW
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_objectchange(self, action):
|
def to_objectchange(self, action):
|
||||||
objectchange = super().to_objectchange(action)
|
objectchange = super().to_objectchange(action)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
@@ -183,7 +183,6 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge_interfaces = NestedInterfaceSerializer(many=True, read_only=True)
|
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
||||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
@@ -223,13 +222,13 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
'id', 'url', 'display_url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled',
|
||||||
'parent', 'bridge', 'bridge_interfaces', 'lag', 'mtu', 'mac_address', 'primary_mac_address',
|
'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'primary_mac_address', 'mac_addresses', 'speed', 'duplex',
|
||||||
'mac_addresses', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'poe_mode', 'poe_type',
|
||||||
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||||
'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end',
|
'vlan_translation_policy', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
|
||||||
'wireless_link', 'link_peers', 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination',
|
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
|
||||||
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
|
||||||
|
|
||||||
|
|||||||
@@ -24,45 +24,35 @@ __all__ = (
|
|||||||
'CableFilterForm',
|
'CableFilterForm',
|
||||||
'ConsoleConnectionFilterForm',
|
'ConsoleConnectionFilterForm',
|
||||||
'ConsolePortFilterForm',
|
'ConsolePortFilterForm',
|
||||||
'ConsolePortTemplateFilterForm',
|
|
||||||
'ConsoleServerPortFilterForm',
|
'ConsoleServerPortFilterForm',
|
||||||
'ConsoleServerPortTemplateFilterForm',
|
|
||||||
'DeviceBayFilterForm',
|
'DeviceBayFilterForm',
|
||||||
'DeviceBayTemplateFilterForm',
|
|
||||||
'DeviceFilterForm',
|
'DeviceFilterForm',
|
||||||
'DeviceRoleFilterForm',
|
'DeviceRoleFilterForm',
|
||||||
'DeviceTypeFilterForm',
|
'DeviceTypeFilterForm',
|
||||||
'FrontPortFilterForm',
|
'FrontPortFilterForm',
|
||||||
'FrontPortTemplateFilterForm',
|
|
||||||
'InterfaceConnectionFilterForm',
|
'InterfaceConnectionFilterForm',
|
||||||
'InterfaceFilterForm',
|
'InterfaceFilterForm',
|
||||||
'InterfaceTemplateFilterForm',
|
|
||||||
'InventoryItemFilterForm',
|
'InventoryItemFilterForm',
|
||||||
'InventoryItemTemplateFilterForm',
|
|
||||||
'InventoryItemRoleFilterForm',
|
'InventoryItemRoleFilterForm',
|
||||||
'LocationFilterForm',
|
'LocationFilterForm',
|
||||||
'MACAddressFilterForm',
|
'MACAddressFilterForm',
|
||||||
'ManufacturerFilterForm',
|
'ManufacturerFilterForm',
|
||||||
'ModuleFilterForm',
|
'ModuleFilterForm',
|
||||||
'ModuleBayFilterForm',
|
'ModuleBayFilterForm',
|
||||||
'ModuleBayTemplateFilterForm',
|
|
||||||
'ModuleTypeFilterForm',
|
'ModuleTypeFilterForm',
|
||||||
'ModuleTypeProfileFilterForm',
|
'ModuleTypeProfileFilterForm',
|
||||||
'PlatformFilterForm',
|
'PlatformFilterForm',
|
||||||
'PowerConnectionFilterForm',
|
'PowerConnectionFilterForm',
|
||||||
'PowerFeedFilterForm',
|
'PowerFeedFilterForm',
|
||||||
'PowerOutletFilterForm',
|
'PowerOutletFilterForm',
|
||||||
'PowerOutletTemplateFilterForm',
|
|
||||||
'PowerPanelFilterForm',
|
'PowerPanelFilterForm',
|
||||||
'PowerPortFilterForm',
|
'PowerPortFilterForm',
|
||||||
'PowerPortTemplateFilterForm',
|
|
||||||
'RackFilterForm',
|
'RackFilterForm',
|
||||||
'RackElevationFilterForm',
|
'RackElevationFilterForm',
|
||||||
'RackReservationFilterForm',
|
'RackReservationFilterForm',
|
||||||
'RackRoleFilterForm',
|
'RackRoleFilterForm',
|
||||||
'RackTypeFilterForm',
|
'RackTypeFilterForm',
|
||||||
'RearPortFilterForm',
|
'RearPortFilterForm',
|
||||||
'RearPortTemplateFilterForm',
|
|
||||||
'RegionFilterForm',
|
'RegionFilterForm',
|
||||||
'SiteFilterForm',
|
'SiteFilterForm',
|
||||||
'SiteGroupFilterForm',
|
'SiteGroupFilterForm',
|
||||||
@@ -1300,23 +1290,6 @@ class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceComponentTemplateFilterForm(NetBoxModelFilterSetForm):
|
|
||||||
device_type_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=DeviceType.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Device type'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModularDeviceComponentTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|
||||||
module_type_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=ModuleType.objects.all(),
|
|
||||||
required=False,
|
|
||||||
query_params={'manufacturer_id': '$manufacturer_id'},
|
|
||||||
label=_('Module Type'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CabledFilterForm(forms.Form):
|
class CabledFilterForm(forms.Form):
|
||||||
cabled = forms.NullBooleanField(
|
cabled = forms.NullBooleanField(
|
||||||
label=_('Cabled'),
|
label=_('Cabled'),
|
||||||
@@ -1369,20 +1342,6 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = ConsolePortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=ConsolePortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1408,20 +1367,6 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = ConsoleServerPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=ConsolePortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1442,20 +1387,6 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = PowerPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PowerPortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1485,20 +1416,6 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = PowerOutletTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PowerOutletTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||||
model = Interface
|
model = Interface
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1626,51 +1543,6 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = InterfaceTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', 'enabled', 'mgmt_only', name=_('Attributes')),
|
|
||||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
|
||||||
FieldSet('rf_role', name=_('Wireless')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=InterfaceTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
enabled = forms.NullBooleanField(
|
|
||||||
label=_('Enabled'),
|
|
||||||
required=False,
|
|
||||||
widget=forms.Select(
|
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mgmt_only = forms.NullBooleanField(
|
|
||||||
label=_('Management only'),
|
|
||||||
required=False,
|
|
||||||
widget=forms.Select(
|
|
||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
poe_mode = forms.MultipleChoiceField(
|
|
||||||
choices=InterfacePoEModeChoices,
|
|
||||||
required=False,
|
|
||||||
label=_('PoE mode')
|
|
||||||
)
|
|
||||||
poe_type = forms.MultipleChoiceField(
|
|
||||||
choices=InterfacePoETypeChoices,
|
|
||||||
required=False,
|
|
||||||
label=_('PoE type')
|
|
||||||
)
|
|
||||||
rf_role = forms.MultipleChoiceField(
|
|
||||||
choices=WirelessRoleChoices,
|
|
||||||
required=False,
|
|
||||||
label=_('Wireless role')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
@@ -1695,24 +1567,6 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = FrontPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
color = ColorField(
|
|
||||||
label=_('Color'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1737,24 +1591,6 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = RearPortTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=PortTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
color = ColorField(
|
|
||||||
label=_('Color'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1773,19 +1609,6 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
|
|
||||||
model = ModuleBayTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
position = forms.CharField(
|
|
||||||
label=_('Position'),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1800,15 +1623,6 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|
||||||
model = DeviceBayTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1856,25 +1670,6 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm):
|
|
||||||
model = InventoryItemTemplate
|
|
||||||
fieldsets = (
|
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
|
||||||
FieldSet('name', 'label', 'role_id', 'manufacturer_id', name=_('Attributes')),
|
|
||||||
FieldSet('device_type_id', name=_('Device')),
|
|
||||||
)
|
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=InventoryItemRole.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Role')
|
|
||||||
)
|
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
|
||||||
queryset=Manufacturer.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label=_('Manufacturer')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device component roles
|
# Device component roles
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ def sync_cached_scope_fields(instance, created, **kwargs):
|
|||||||
for model in (Prefix, Cluster, WirelessLAN):
|
for model in (Prefix, Cluster, WirelessLAN):
|
||||||
qs = model.objects.filter(**filters)
|
qs = model.objects.filter(**filters)
|
||||||
|
|
||||||
for obj in qs:
|
for obj in qs.only('id'):
|
||||||
# Recompute cache using the same logic as save()
|
# Recompute cache using the same logic as save()
|
||||||
obj.cache_related_objects()
|
obj.cache_related_objects()
|
||||||
obj.save(update_fields=[
|
obj.save(update_fields=[
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from core.models import ObjectType
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField
|
||||||
from ipam.models import Prefix
|
|
||||||
from netbox.choices import WeightUnitChoices
|
from netbox.choices import WeightUnitChoices
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.data import drange
|
from utilities.data import drange
|
||||||
@@ -1193,14 +1192,3 @@ class VirtualChassisTestCase(TestCase):
|
|||||||
device2.vc_position = 1
|
device2.vc_position = 1
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
device2.full_clean()
|
device2.full_clean()
|
||||||
|
|
||||||
|
|
||||||
class SiteSignalTestCase(TestCase):
|
|
||||||
|
|
||||||
@tag('regression')
|
|
||||||
def test_edit_site_with_prefix_no_vrf(self):
|
|
||||||
site = Site.objects.create(name='Test Site', slug='test-site')
|
|
||||||
Prefix.objects.create(prefix='192.0.2.0/24', scope=site, vrf=None)
|
|
||||||
|
|
||||||
# Regression test for #21045: should not raise ValueError
|
|
||||||
site.save()
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from django.views.generic import View
|
|||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||||
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
|
||||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||||
from netbox.object_actions import *
|
from netbox.object_actions import *
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
@@ -2900,7 +2900,6 @@ class InterfaceView(generic.ObjectView):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'vdc_table': vdc_table,
|
'vdc_table': vdc_table,
|
||||||
'bridge_interfaces': bridge_interfaces,
|
|
||||||
'bridge_interfaces_table': bridge_interfaces_table,
|
'bridge_interfaces_table': bridge_interfaces_table,
|
||||||
'child_interfaces_table': child_interfaces_table,
|
'child_interfaces_table': child_interfaces_table,
|
||||||
'vlan_table': vlan_table,
|
'vlan_table': vlan_table,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from django.db.models import ForeignKey
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -176,21 +175,6 @@ class BulkEdit(ObjectAction):
|
|||||||
permissions_required = {'change'}
|
permissions_required = {'change'}
|
||||||
template_name = 'buttons/bulk_edit.html'
|
template_name = 'buttons/bulk_edit.html'
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_context(cls, context, model):
|
|
||||||
url_params = super().get_url_params(context)
|
|
||||||
|
|
||||||
# If this is a child object, pass the parent's PK as a URL parameter
|
|
||||||
if parent := context.get('object'):
|
|
||||||
for field in model._meta.get_fields():
|
|
||||||
if isinstance(field, ForeignKey) and field.remote_field.model == parent.__class__:
|
|
||||||
url_params[field.name] = parent.pk
|
|
||||||
break
|
|
||||||
|
|
||||||
return {
|
|
||||||
'url_params': url_params,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BulkRename(ObjectAction):
|
class BulkRename(ObjectAction):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from unittest import skipIf
|
from unittest import skipIf
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import Device, DeviceType, Manufacturer
|
from dcim.models import Device
|
||||||
from netbox.object_actions import AddObject, BulkEdit, BulkImport
|
from netbox.object_actions import AddObject, BulkImport
|
||||||
|
from netbox.tests.dummy_plugin.models import DummyNetBoxModel
|
||||||
|
|
||||||
|
|
||||||
class ObjectActionTest(TestCase):
|
class ObjectActionTest(TestCase):
|
||||||
@@ -19,11 +20,9 @@ class ObjectActionTest(TestCase):
|
|||||||
url = BulkImport.get_url(obj)
|
url = BulkImport.get_url(obj)
|
||||||
self.assertEqual(url, '/dcim/devices/import/')
|
self.assertEqual(url, '/dcim/devices/import/')
|
||||||
|
|
||||||
@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, 'dummy_plugin not in settings.PLUGINS')
|
@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
|
||||||
def test_get_url_plugin_model(self):
|
def test_get_url_plugin_model(self):
|
||||||
"""Test URL generation for plugin models includes plugins: namespace"""
|
"""Test URL generation for plugin models includes plugins: namespace"""
|
||||||
from netbox.tests.dummy_plugin.models import DummyNetBoxModel
|
|
||||||
|
|
||||||
obj = DummyNetBoxModel()
|
obj = DummyNetBoxModel()
|
||||||
|
|
||||||
url = AddObject.get_url(obj)
|
url = AddObject.get_url(obj)
|
||||||
@@ -31,29 +30,3 @@ class ObjectActionTest(TestCase):
|
|||||||
|
|
||||||
url = BulkImport.get_url(obj)
|
url = BulkImport.get_url(obj)
|
||||||
self.assertEqual(url, '/plugins/dummy-plugin/netboxmodel/import/')
|
self.assertEqual(url, '/plugins/dummy-plugin/netboxmodel/import/')
|
||||||
|
|
||||||
def test_bulk_edit_get_context_child_object(self):
|
|
||||||
"""
|
|
||||||
Test that the parent object's PK is included in the context for child objects.
|
|
||||||
|
|
||||||
Ensure that BulkEdit.get_context() correctly identifies and
|
|
||||||
includes the parent object's PK when rendering a child object's
|
|
||||||
action button.
|
|
||||||
"""
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
|
||||||
|
|
||||||
# Mock context containing the parent object (DeviceType)
|
|
||||||
request = RequestFactory().get('/')
|
|
||||||
context = {
|
|
||||||
'request': request,
|
|
||||||
'object': device_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get context for the child model (Device)
|
|
||||||
action_context = BulkEdit.get_context(context, Device)
|
|
||||||
|
|
||||||
# Verify that 'device_type' (the FK field name) is present in
|
|
||||||
# url_params with the parent's PK
|
|
||||||
self.assertIn('url_params', action_context)
|
|
||||||
self.assertEqual(action_context['url_params'].get('device_type'), device_type.pk)
|
|
||||||
|
|||||||
10
netbox/project-static/dist/netbox.js
vendored
10
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -112,19 +112,6 @@
|
|||||||
<th scope="row">{% trans "Bridge" %}</th>
|
<th scope="row">{% trans "Bridge" %}</th>
|
||||||
<td>{{ object.bridge|linkify|placeholder }}</td>
|
<td>{{ object.bridge|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Bridged Interfaces" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if bridge_interfaces %}
|
|
||||||
{% for interface in bridge_interfaces %}
|
|
||||||
{{ interface|linkify }}
|
|
||||||
{% if not forloop.last %}<br />{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
{{ ''|placeholder }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "LAG" %}</th>
|
<th scope="row">{% trans "LAG" %}</th>
|
||||||
<td>{{ object.lag|linkify|placeholder }}</td>
|
<td>{{ object.lag|linkify|placeholder }}</td>
|
||||||
@@ -448,11 +435,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row mb-3">
|
{% if object.is_bridge %}
|
||||||
<div class="col col-md-12">
|
<div class="row mb-3">
|
||||||
{% include 'inc/panel_table.html' with table=bridge_interfaces_table heading="Bridged Interfaces" %}
|
<div class="col col-md-12">
|
||||||
|
{% include 'inc/panel_table.html' with table=bridge_interfaces_table heading="Bridge Interfaces" %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
|
{% include 'inc/panel_table.html' with table=child_interfaces_table heading="Child Interfaces" %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user