Merge branch 'feature' into 13150-table-translation

This commit is contained in:
Arthur 2023-07-28 15:43:26 +07:00
commit b36be1a9e5
11 changed files with 79 additions and 10 deletions

View File

@ -514,12 +514,18 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
allow_blank=True, allow_blank=True,
allow_null=True allow_null=True
) )
rf_role = ChoiceField(
choices=WirelessRoleChoices,
required=False,
allow_blank=True,
allow_null=True
)
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only',
'description', 'bridge', 'poe_mode', 'poe_type', 'created', 'last_updated', 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
] ]

View File

@ -696,6 +696,9 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
poe_type = django_filters.MultipleChoiceFilter( poe_type = django_filters.MultipleChoiceFilter(
choices=InterfacePoETypeChoices choices=InterfacePoETypeChoices
) )
rf_role = django_filters.MultipleChoiceFilter(
choices=WirelessRoleChoices
)
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate

View File

@ -76,14 +76,14 @@ class PowerOutletBulkCreateForm(
class InterfaceBulkCreateForm( class InterfaceBulkCreateForm(
form_from_model(Interface, [ form_from_model(Interface, [
'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', 'rf_role'
]), ]),
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = Interface model = Interface
field_order = ( field_order = (
'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode', 'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
'poe_type', 'mark_connected', 'description', 'tags', 'poe_type', 'mark_connected', 'rf_role', 'description', 'tags',
) )

View File

@ -15,6 +15,7 @@ from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
from wireless.choices import WirelessRoleChoices
__all__ = ( __all__ = (
'CableBulkEditForm', 'CableBulkEditForm',
@ -922,8 +923,14 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
initial='', initial='',
label=_('PoE type') label=_('PoE type')
) )
rf_role = forms.ChoiceField(
choices=add_blank_choice(WirelessRoleChoices),
required=False,
initial='',
label=_('Wireless role')
)
nullable_fields = ('label', 'description', 'poe_mode', 'poe_type') nullable_fields = ('label', 'description', 'poe_mode', 'poe_type', 'rf_role')
class FrontPortTemplateBulkEditForm(BulkEditForm): class FrontPortTemplateBulkEditForm(BulkEditForm):

View File

@ -826,13 +826,14 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')), (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
('PoE', ('poe_mode', 'poe_type')) ('PoE', ('poe_mode', 'poe_type')),
('Wireless', ('rf_role',))
) )
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge', 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge', 'rf_role',
] ]

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext as _
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
from dcim.models import * from dcim.models import *
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from wireless.choices import WirelessRoleChoices
__all__ = ( __all__ = (
'ConsolePortTemplateImportForm', 'ConsolePortTemplateImportForm',
@ -96,11 +97,17 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
required=False, required=False,
label=_('PoE type') label=_('PoE type')
) )
rf_role = forms.ChoiceField(
choices=WirelessRoleChoices,
required=False,
label=_('Wireless role')
)
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'poe_mode',
'poe_type', 'rf_role'
] ]

View File

@ -277,6 +277,9 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
def resolve_poe_type(self, info): def resolve_poe_type(self, info):
return self.poe_type or None return self.poe_type or None
def resolve_rf_role(self, info):
return self.rf_role or None
class InventoryItemType(ComponentObjectType): class InventoryItemType(ComponentObjectType):
component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType') component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-18 07:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0178_virtual_chassis_member_counter'),
]
operations = [
migrations.AddField(
model_name='interfacetemplate',
name='rf_role',
field=models.CharField(blank=True, max_length=30),
),
]

View File

@ -13,6 +13,7 @@ from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface from utilities.ordering import naturalize_interface
from utilities.tracking import TrackingModelMixin from utilities.tracking import TrackingModelMixin
from wireless.choices import WirelessRoleChoices
from .device_components import ( from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort, RearPort,
@ -388,6 +389,12 @@ class InterfaceTemplate(ModularComponentTemplateModel):
blank=True, blank=True,
verbose_name='PoE type' verbose_name='PoE type'
) )
rf_role = models.CharField(
max_length=30,
choices=WirelessRoleChoices,
blank=True,
verbose_name='Wireless role'
)
component_model = Interface component_model = Interface
@ -406,6 +413,11 @@ class InterfaceTemplate(ModularComponentTemplateModel):
'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type" 'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type"
}) })
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
raise ValidationError({
'rf_role': "Wireless role may be set only on wireless interfaces."
})
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
@ -415,6 +427,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
mgmt_only=self.mgmt_only, mgmt_only=self.mgmt_only,
poe_mode=self.poe_mode, poe_mode=self.poe_mode,
poe_type=self.poe_type, poe_type=self.poe_type,
rf_role=self.rf_role,
**kwargs **kwargs
) )
instantiate.do_not_call_in_templates = True instantiate.do_not_call_in_templates = True
@ -430,6 +443,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
'bridge': self.bridge.name if self.bridge else None, 'bridge': self.bridge.name if self.bridge else None,
'poe_mode': self.poe_mode, 'poe_mode': self.poe_mode,
'poe_type': self.poe_type, 'poe_type': self.poe_type,
'rf_role': self.rf_role,
} }

View File

@ -229,7 +229,10 @@ class InterfaceTemplateTable(ComponentTemplateTable):
class Meta(ComponentTemplateTable.Meta): class Meta(ComponentTemplateTable.Meta):
model = models.InterfaceTemplate model = models.InterfaceTemplate
fields = ('pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type', 'actions') fields = (
'pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type',
'rf_role', 'actions',
)
empty_text = "None" empty_text = "None"

View File

@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from django.test import override_settings from django.test import override_settings
from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLUnion from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLUnion, String as GQLString, NonNull as GQLNonNull
from rest_framework import status from rest_framework import status
from rest_framework.test import APIClient from rest_framework.test import APIClient
@ -452,6 +452,13 @@ class APIViewTestCases:
# Compile list of fields to include # Compile list of fields to include
fields_string = '' fields_string = ''
for field_name, field in type_class._meta.fields.items(): for field_name, field in type_class._meta.fields.items():
is_string_array = False
if type(field.type) is GQLList:
if field.type.of_type is GQLString:
is_string_array = True
elif type(field.type.of_type) is GQLNonNull and field.type.of_type.of_type is GQLString:
is_string_array = True
if type(field) is GQLDynamic: if type(field) is GQLDynamic:
# Dynamic fields must specify a subselection # Dynamic fields must specify a subselection
fields_string += f'{field_name} {{ id }}\n' fields_string += f'{field_name} {{ id }}\n'
@ -461,7 +468,7 @@ class APIViewTestCases:
elif type(field.type) is GQLList and inspect.isclass(field.type.of_type) and issubclass(field.type.of_type, GQLUnion): elif type(field.type) is GQLList and inspect.isclass(field.type.of_type) and issubclass(field.type.of_type, GQLUnion):
# Union types dont' have an id or consistent values # Union types dont' have an id or consistent values
continue continue
elif type(field.type) is GQLList and field_name != 'choices': elif type(field.type) is GQLList and not is_string_array:
# TODO: Come up with something more elegant # TODO: Come up with something more elegant
# Temporary hack to support automated testing of reverse generic relations # Temporary hack to support automated testing of reverse generic relations
fields_string += f'{field_name} {{ id }}\n' fields_string += f'{field_name} {{ id }}\n'