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 netbox.graphql.filter_lookups import IntegerLookup
from extras.graphql.filters import ConfigTemplateFilter from extras.graphql.filters import ConfigTemplateFilter
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
from dcim.graphql.filters import LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from .filters import * from .filters import *
__all__ = ( __all__ = (
@@ -36,20 +35,6 @@ class ScopedFilterMixin:
) )
scope_id: ID | None = strawberry_django.filter_field() 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 @dataclass
class ComponentModelFilterMixin: 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_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
outer_height = attrs.NumericAttr('outer_height', 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') 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): class RackNumberingPanel(panels.ObjectAttributesPanel):

View File

@@ -4,17 +4,6 @@ from extras.choices import LogLevelChoices
# Custom fields # Custom fields
CUSTOMFIELD_EMPTY_VALUES = (None, '', []) 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 # Template Export
DEFAULT_MIME_TYPE = 'text/plain; charset=utf-8' 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.forms.mixins import SyncedDataMixin
from core.models import ObjectType from core.models import ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup 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.choices import *
from extras.models import * from extras.models import *
from netbox.events import get_event_type_choices from netbox.events import get_event_type_choices
@@ -785,11 +784,8 @@ class ImageAttachmentForm(forms.ModelForm):
fields = [ fields = [
'image', 'name', 'description', 'image', 'name', 'description',
] ]
# Explicitly set 'image/avif' to support AVIF selection in Firefox help_texts = {
widgets = { 'name': _("If no name is specified, the file name will be used.")
'image': forms.ClearableFileInput(
attrs={'accept': ','.join(sorted(set(IMAGE_ATTACHMENT_IMAGE_FORMATS.values())))}
),
} }

View File

@@ -10,7 +10,6 @@ from taggit.managers import _TaggableManager
from netbox.context import current_request from netbox.context import current_request
from .constants import IMAGE_ATTACHMENT_IMAGE_FORMATS
from .validators import CustomValidator from .validators import CustomValidator
__all__ = ( __all__ = (
@@ -79,7 +78,7 @@ def image_upload(instance, filename):
""" """
upload_dir = 'image-attachments' upload_dir = 'image-attachments'
default_filename = 'unnamed' 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. # Normalize Windows paths and create a Path object.
normalized_filename = str(filename).replace('\\', '/') normalized_filename = str(filename).replace('\\', '/')

View File

@@ -20,7 +20,7 @@ from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface from virtualization.models import VMInterface
if TYPE_CHECKING: 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 circuits.graphql.filters import ProviderFilter
from core.graphql.filters import ContentTypeFilter from core.graphql.filters import ContentTypeFilter
from dcim.graphql.filters import SiteFilter from dcim.graphql.filters import SiteFilter
@@ -53,7 +53,7 @@ __all__ = (
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter): class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | 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() 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() strawberry_django.filter_field()
) )
sites: ( sites: (
@@ -70,10 +70,10 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | 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() 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() 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() 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 django_tables2.utils import Accessor
from dcim.models import Interface from dcim.models import Interface
from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION
from ipam.models import * from ipam.models import *
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
from tenancy.tables import TenancyColumnsMixin, TenantColumn from tenancy.tables import TenancyColumnsMixin, TenantColumn
@@ -159,11 +160,26 @@ class VLANDevicesTable(VLANMembersTable):
actions = columns.ActionsColumn( actions = columns.ActionsColumn(
actions=('edit',) 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): class Meta(NetBoxTable.Meta):
model = Interface model = Interface
fields = ('device', 'name', 'tagged', 'actions') fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions')
exclude = ('id', ) default_columns = ('device', 'name', 'connection', 'tagged', 'actions')
exclude = ('id',)
class VLANVirtualMachinesTable(VLANMembersTable): class VLANVirtualMachinesTable(VLANMembersTable):

View File

@@ -19,11 +19,8 @@ from strawberry_django import (
process_filters, process_filters,
) )
from netbox.graphql.scalars import BigInt
__all__ = ( __all__ = (
'ArrayLookup', 'ArrayLookup',
'BigIntegerLookup',
'FloatArrayLookup', 'FloatArrayLookup',
'FloatLookup', 'FloatLookup',
'IntegerArrayLookup', 'IntegerArrayLookup',
@@ -81,29 +78,6 @@ class IntegerLookup:
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix) 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.') @strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.')
class FloatLookup: class FloatLookup:
filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field() filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field()

View File

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

View File

@@ -15,7 +15,7 @@ from vpn import models
if TYPE_CHECKING: if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter from core.graphql.filters import ContentTypeFilter
from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter 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 * from .enums import *
__all__ = ( __all__ = (
@@ -75,7 +75,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field() 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() strawberry_django.filter_field()
) )
terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( 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 = ( type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field() 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() strawberry_django.filter_field()
) )
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (