Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Hauser
8f468b4bbf feat(ipam): Add connection/link peer to VLANDeviceTable
The VLAN Device Interfaces table now includes `connection` and
`link_peer` columns, using the existing interface templates to render
peer/connection context consistently.

Fixes #15801
2026-01-19 14:27:38 +01:00
10 changed files with 32 additions and 74 deletions

View File

@@ -13,7 +13,6 @@ if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup
from extras.graphql.filters import ConfigTemplateFilter
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
from dcim.graphql.filters import LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from .filters import *
__all__ = (
@@ -36,20 +35,6 @@ class ScopedFilterMixin:
)
scope_id: ID | None = strawberry_django.filter_field()
# Cached relations
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='location')
)
_region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='region')
)
_site_group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site_group')
)
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site')
)
@dataclass
class ComponentModelFilterMixin:

View File

@@ -31,7 +31,7 @@ class RackDimensionsPanel(panels.ObjectAttributesPanel):
outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display')
outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display')
mounting_depth = attrs.TextAttr('mounting_depth', format_string=_('{} millimeters'))
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{} mm')
class RackNumberingPanel(panels.ObjectAttributesPanel):

View File

@@ -4,17 +4,6 @@ from extras.choices import LogLevelChoices
# Custom fields
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])
# ImageAttachment
IMAGE_ATTACHMENT_IMAGE_FORMATS = {
'avif': 'image/avif',
'bmp': 'image/bmp',
'gif': 'image/gif',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'png': 'image/png',
'webp': 'image/webp',
}
# Template Export
DEFAULT_MIME_TYPE = 'text/plain; charset=utf-8'

View File

@@ -9,7 +9,6 @@ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
from extras.choices import *
from extras.models import *
from netbox.events import get_event_type_choices
@@ -785,11 +784,8 @@ class ImageAttachmentForm(forms.ModelForm):
fields = [
'image', 'name', 'description',
]
# Explicitly set 'image/avif' to support AVIF selection in Firefox
widgets = {
'image': forms.ClearableFileInput(
attrs={'accept': ','.join(sorted(set(IMAGE_ATTACHMENT_IMAGE_FORMATS.values())))}
),
help_texts = {
'name': _("If no name is specified, the file name will be used.")
}

View File

@@ -10,7 +10,6 @@ from taggit.managers import _TaggableManager
from netbox.context import current_request
from .constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
from .validators import CustomValidator
__all__ = (
@@ -79,7 +78,7 @@ def image_upload(instance, filename):
"""
upload_dir = 'image-attachments'
default_filename = 'unnamed'
allowed_img_extensions = IMAGE_ATTACHMENT_IMAGE_FORMATS.keys()
allowed_img_extensions = ('bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp')
# Normalize Windows paths and create a Path object.
normalized_filename = str(filename).replace('\\', '/')

View File

@@ -20,7 +20,7 @@ from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface
if TYPE_CHECKING:
from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup, IntegerRangeArrayLookup
from netbox.graphql.filter_lookups import IntegerLookup, IntegerRangeArrayLookup
from circuits.graphql.filters import ProviderFilter
from core.graphql.filters import ContentTypeFilter
from dcim.graphql.filters import SiteFilter
@@ -53,7 +53,7 @@ __all__ = (
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
rir_id: ID | None = strawberry_django.filter_field()
asn: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
sites: (
@@ -70,10 +70,10 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
slug: FilterLookup[str] | None = strawberry_django.filter_field()
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
rir_id: ID | None = strawberry_django.filter_field()
start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
start: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
end: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
end: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)

View File

@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from django_tables2.utils import Accessor
from dcim.models import Interface
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
from ipam.models import *
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
from tenancy.tables import TenancyColumnsMixin, TenantColumn
@@ -159,11 +160,26 @@ class VLANDevicesTable(VLANMembersTable):
actions = columns.ActionsColumn(
actions=('edit',)
)
link_peer = columns.TemplateColumn(
accessor='link_peers',
template_code=LINKTERMINATION,
orderable=False,
verbose_name=_('Link Peers'),
)
# Override PathEndpointTable.connection to accommodate virtual circuits
connection = columns.TemplateColumn(
accessor='_path__destinations',
template_code=INTERFACE_LINKTERMINATION,
orderable=False,
verbose_name=_('Connection'),
)
class Meta(NetBoxTable.Meta):
model = Interface
fields = ('device', 'name', 'tagged', 'actions')
exclude = ('id', )
fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions')
default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
exclude = ('id',)
class VLANVirtualMachinesTable(VLANMembersTable):

View File

@@ -19,11 +19,8 @@ from strawberry_django import (
process_filters,
)
from netbox.graphql.scalars import BigInt
__all__ = (
'ArrayLookup',
'BigIntegerLookup',
'FloatArrayLookup',
'FloatLookup',
'IntegerArrayLookup',
@@ -81,29 +78,6 @@ class IntegerLookup:
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
@strawberry.input(one_of=True, description='Lookup for BigInteger fields. Only one of the lookup fields can be set.')
class BigIntegerLookup:
filter_lookup: FilterLookup[BigInt] | None = strawberry_django.filter_field()
range_lookup: RangeLookup[BigInt] | None = strawberry_django.filter_field()
comparison_lookup: ComparisonFilterLookup[BigInt] | None = strawberry_django.filter_field()
def get_filter(self):
for field in self.__strawberry_definition__.fields:
value = getattr(self, field.name, None)
if value is not strawberry.UNSET:
return value
return None
@strawberry_django.filter_field
def filter(self, info: Info, queryset: QuerySet, prefix: DirectiveValue[str] = '') -> Tuple[QuerySet, Q]:
filters = self.get_filter()
if not filters:
return queryset, Q()
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
@strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.')
class FloatLookup:
filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field()

View File

@@ -3,10 +3,9 @@ import string
from django.db.models import Q
OBJECTPERMISSION_OBJECT_TYPES = (
(Q(public=True) & ~Q(app_label='core', model='objecttype'))
| Q(app_label='core', model__in=['managedfile'])
| Q(app_label='extras', model__in=['scriptmodule', 'taggeditem'])
OBJECTPERMISSION_OBJECT_TYPES = Q(
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) |
Q(app_label='users', model__in=['objectpermission', 'token', 'group', 'user', 'owner'])
)
CONSTRAINT_TOKEN_USER = '$user'

View File

@@ -15,7 +15,7 @@ from vpn import models
if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter
from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter
from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup
from netbox.graphql.filter_lookups import IntegerLookup
from .enums import *
__all__ = (
@@ -75,7 +75,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field()
)
tunnel_id: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
tunnel_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
@@ -187,7 +187,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
identifier: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (