Compare commits

..

6 Commits

Author SHA1 Message Date
bctiemann
0f62137957 Merge pull request #21199 from netbox-community/21178-change-rack-dimensions-display-to-be-more-consistent
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Fixes #21178: Use localized “millimeters” for rack mounting depth (follow-up)
2026-01-19 14:14:24 -05:00
Martin Hauser
7858ccb712 feat(extras): Add AVIF support for image attachments
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Extends allowed image file formats to include AVIF for better modern
format support. Introduces a constants mapping for image formats to
centralize file type definitions. Updates form widgets and utilities
to leverage the new constants, enabling more flexible and consistent
image handling.

Fixes #21039
2026-01-19 09:56:06 -05:00
Martin Hauser
6b7b38ee0a fix(users): Refactor object permission query logic
Simplifies the `OBJECTPERMISSION_OBJECT_TYPES` definition by adjusting
query filters and introducing new conditions for specific app labels
and models.

Fixes #21051
2026-01-19 09:30:36 -05:00
matthew-242
c8f17e06a2 Add support to filter on cached relations _location, _region, _site and _site_group to ScopedFilterMixin (#21162) 2026-01-19 09:09:03 -05:00
Jeremy Stretch
edace6aff4 Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API (#21186)
* Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API

* tunnel_id should also use BigIntegerLookup
2026-01-19 08:54:39 -05:00
Martin Hauser
3d1f18d6dd fix(dcim): Localize mounting depth format string
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Replaces the fixed format string for `mounting_depth` with a localized
version using `gettext_lazy`. This ensures proper translation of the
unit label for internationalization purposes.

Fixes #21178
2026-01-16 19:53:49 +01:00
10 changed files with 74 additions and 32 deletions

View File

@@ -13,6 +13,7 @@ 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__ = (
@@ -35,6 +36,20 @@ 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='{} mm') mounting_depth = attrs.TextAttr('mounting_depth', format_string=_('{} millimeters'))
class RackNumberingPanel(panels.ObjectAttributesPanel): class RackNumberingPanel(panels.ObjectAttributesPanel):

View File

@@ -4,6 +4,17 @@ 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,6 +9,7 @@ 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
@@ -784,8 +785,11 @@ class ImageAttachmentForm(forms.ModelForm):
fields = [ fields = [
'image', 'name', 'description', 'image', 'name', 'description',
] ]
help_texts = { # Explicitly set 'image/avif' to support AVIF selection in Firefox
'name': _("If no name is specified, the file name will be used.") widgets = {
'image': forms.ClearableFileInput(
attrs={'accept': ','.join(sorted(set(IMAGE_ATTACHMENT_IMAGE_FORMATS.values())))}
),
} }

View File

@@ -10,6 +10,7 @@ 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__ = (
@@ -78,7 +79,7 @@ def image_upload(instance, filename):
""" """
upload_dir = 'image-attachments' upload_dir = 'image-attachments'
default_filename = 'unnamed' default_filename = 'unnamed'
allowed_img_extensions = ('bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp') allowed_img_extensions = IMAGE_ATTACHMENT_IMAGE_FORMATS.keys()
# 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 IntegerLookup, IntegerRangeArrayLookup from netbox.graphql.filter_lookups import BigIntegerLookup, 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['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( asn: Annotated['BigIntegerLookup', 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['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
end: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( end: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )

View File

@@ -4,7 +4,6 @@ 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
@@ -160,26 +159,11 @@ 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', 'link_peer', 'connection', 'tagged', 'actions') fields = ('device', 'name', 'tagged', 'actions')
default_columns = ('device', 'name', 'connection', 'tagged', 'actions') exclude = ('id', )
exclude = ('id',)
class VLANVirtualMachinesTable(VLANMembersTable): class VLANVirtualMachinesTable(VLANMembersTable):

View File

@@ -19,8 +19,11 @@ 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',
@@ -78,6 +81,29 @@ 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,9 +3,10 @@ import string
from django.db.models import Q from django.db.models import Q
OBJECTPERMISSION_OBJECT_TYPES = Q( OBJECTPERMISSION_OBJECT_TYPES = (
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) | (Q(public=True) & ~Q(app_label='core', model='objecttype'))
Q(app_label='users', model__in=['objectpermission', 'token', 'group', 'user', 'owner']) | Q(app_label='core', model__in=['managedfile'])
| 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 IntegerLookup from netbox.graphql.filter_lookups import BigIntegerLookup, 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['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( tunnel_id: Annotated['BigIntegerLookup', 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['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( identifier: Annotated['BigIntegerLookup', 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 = (