Merge branch 'feature' into 7848-rq-api

This commit is contained in:
Arthur Hanson 2024-11-18 07:41:36 -08:00
commit d3953a2f6d
83 changed files with 1125 additions and 496 deletions

View File

@ -43,3 +43,7 @@ The security cipher used to apply wireless authentication. Options include:
### Pre-Shared Key
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
### Scope
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this wireless LAN is associated.

View File

@ -21,6 +21,7 @@ def copy_site_assignments(apps, schema_editor):
termination_id=models.F('provider_network_id')
)
class Migration(migrations.Migration):
dependencies = [

View File

@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0048_circuitterminations_cached_relations'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='provider',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='providernetwork',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
]

View File

@ -21,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_('Full name of the provider')
help_text=_('Full name of the provider'),
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),
@ -95,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
provider = models.ForeignKey(
to='circuits.Provider',

View File

@ -76,10 +76,6 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_update_object(self):
site = Site(name='Site 1', slug='site-1')
site.save()
@ -117,12 +113,6 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_delete_object(self):
site = Site(
name='Site 1',
@ -153,10 +143,6 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
self.assertEqual(oc.postchange_data, None)
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
def test_bulk_update_objects(self):
sites = (
Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
@ -353,10 +339,6 @@ class ChangeLogAPITest(APITestCase):
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_update_object(self):
site = Site(name='Site 1', slug='site-1')
site.save()
@ -389,12 +371,6 @@ class ChangeLogAPITest(APITestCase):
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_delete_object(self):
site = Site(
name='Site 1',
@ -423,10 +399,6 @@ class ChangeLogAPITest(APITestCase):
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
self.assertEqual(oc.postchange_data, None)
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
def test_bulk_create_objects(self):
data = (
{

View File

@ -21,7 +21,7 @@ __all__ = (
class RegionSerializer(NestedGroupModelSerializer):
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True, default=0)
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
class Meta:
model = Region
@ -35,7 +35,7 @@ class RegionSerializer(NestedGroupModelSerializer):
class SiteGroupSerializer(NestedGroupModelSerializer):
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True, default=0)
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
class Meta:
model = SiteGroup
@ -63,7 +63,7 @@ class SiteSerializer(NetBoxModelSerializer):
# Related object counts
circuit_count = RelatedObjectCountField('circuit_terminations')
device_count = RelatedObjectCountField('devices')
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
rack_count = RelatedObjectCountField('racks')
vlan_count = RelatedObjectCountField('vlans')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
@ -86,7 +86,7 @@ class LocationSerializer(NestedGroupModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True, default=0)
device_count = serializers.IntegerField(read_only=True, default=0)
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
class Meta:
model = Location

View File

@ -0,0 +1,67 @@
import django_filters
from django.utils.translation import gettext as _
from netbox.filtersets import BaseFilterSet
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
from .models import *
__all__ = (
'ScopedFilterSet',
)
class ScopedFilterSet(BaseFilterSet):
"""
Provides additional filtering functionality for location, site, etc.. for Scoped models.
"""
scope_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)

View File

@ -73,7 +73,6 @@ __all__ = (
'RearPortFilterSet',
'RearPortTemplateFilterSet',
'RegionFilterSet',
'ScopedFilterSet',
'SiteFilterSet',
'SiteGroupFilterSet',
'VirtualChassisFilterSet',
@ -2345,60 +2344,3 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet):
class Meta:
model = Interface
fields = tuple()
class ScopedFilterSet(BaseFilterSet):
"""
Provides additional filtering functionality for location, site, etc.. for Scoped models.
"""
scope_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)

View File

@ -76,7 +76,6 @@ class ComponentType(
"""
Base type for device/VM components
"""
_name: str
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
@ -93,7 +92,6 @@ class ComponentTemplateType(
"""
Base type for device/VM components
"""
_name: str
device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]
@ -181,7 +179,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
filters=ConsolePortTemplateFilter
)
class ConsolePortTemplateType(ModularComponentTemplateType):
_name: str
pass
@strawberry_django.type(
@ -199,7 +197,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
filters=ConsoleServerPortTemplateFilter
)
class ConsoleServerPortTemplateType(ModularComponentTemplateType):
_name: str
pass
@strawberry_django.type(
@ -208,7 +206,6 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
filters=DeviceFilter
)
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
_name: str
console_port_count: BigInt
console_server_port_count: BigInt
power_port_count: BigInt
@ -273,7 +270,7 @@ class DeviceBayType(ComponentType):
filters=DeviceBayTemplateFilter
)
class DeviceBayTemplateType(ComponentTemplateType):
_name: str
pass
@strawberry_django.type(
@ -282,7 +279,6 @@ class DeviceBayTemplateType(ComponentTemplateType):
filters=InventoryItemTemplateFilter
)
class InventoryItemTemplateType(ComponentTemplateType):
_name: str
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
@ -366,7 +362,6 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
filters=FrontPortTemplateFilter
)
class FrontPortTemplateType(ModularComponentTemplateType):
_name: str
color: str
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
@ -377,6 +372,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
filters=InterfaceFilter
)
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
_name: str
mac_address: str | None
wwn: str | None
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
@ -465,7 +461,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
@ -527,7 +523,7 @@ class ModuleBayType(ModularComponentType):
filters=ModuleBayTemplateFilter
)
class ModuleBayTemplateType(ModularComponentTemplateType):
_name: str
pass
@strawberry_django.type(
@ -588,7 +584,6 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
filters=PowerOutletTemplateFilter
)
class PowerOutletTemplateType(ModularComponentTemplateType):
_name: str
power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
@ -620,8 +615,6 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
filters=PowerPortTemplateFilter
)
class PowerPortTemplateType(ModularComponentTemplateType):
_name: str
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
@ -640,7 +633,6 @@ class RackTypeType(NetBoxObjectType):
filters=RackFilter
)
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
_name: str
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@ -693,7 +685,6 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
filters=RearPortTemplateFilter
)
class RearPortTemplateType(ModularComponentTemplateType):
_name: str
color: str
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@ -716,7 +707,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
@ -729,7 +720,6 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
filters=SiteFilter
)
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
_name: str
time_zone: str | None
region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None
@ -749,7 +739,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
@ -773,7 +763,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:

View File

@ -0,0 +1,17 @@
from django.contrib.postgres.operations import CreateCollation
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0196_qinq_svlan'),
]
operations = [
CreateCollation(
"natural_sort",
provider="icu",
locale="und-u-kn-true",
),
]

View File

@ -0,0 +1,318 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterModelOptions(
name='site',
options={'ordering': ('name',)},
),
migrations.AlterField(
model_name='site',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterModelOptions(
name='consoleport',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='consoleporttemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='consoleserverport',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='consoleserverporttemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='device',
options={'ordering': ('name', 'pk')},
),
migrations.AlterModelOptions(
name='devicebay',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='devicebaytemplate',
options={'ordering': ('device_type', 'name')},
),
migrations.AlterModelOptions(
name='frontport',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='frontporttemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='interfacetemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='inventoryitem',
options={'ordering': ('device__id', 'parent__id', 'name')},
),
migrations.AlterModelOptions(
name='inventoryitemtemplate',
options={'ordering': ('device_type__id', 'parent__id', 'name')},
),
migrations.AlterModelOptions(
name='modulebay',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='modulebaytemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='poweroutlet',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='poweroutlettemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='powerport',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='powerporttemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.AlterModelOptions(
name='rack',
options={'ordering': ('site', 'location', 'name', 'pk')},
),
migrations.AlterModelOptions(
name='rearport',
options={'ordering': ('device', 'name')},
),
migrations.AlterModelOptions(
name='rearporttemplate',
options={'ordering': ('device_type', 'module_type', 'name')},
),
migrations.RemoveField(
model_name='consoleport',
name='_name',
),
migrations.RemoveField(
model_name='consoleporttemplate',
name='_name',
),
migrations.RemoveField(
model_name='consoleserverport',
name='_name',
),
migrations.RemoveField(
model_name='consoleserverporttemplate',
name='_name',
),
migrations.RemoveField(
model_name='device',
name='_name',
),
migrations.RemoveField(
model_name='devicebay',
name='_name',
),
migrations.RemoveField(
model_name='devicebaytemplate',
name='_name',
),
migrations.RemoveField(
model_name='frontport',
name='_name',
),
migrations.RemoveField(
model_name='frontporttemplate',
name='_name',
),
migrations.RemoveField(
model_name='inventoryitem',
name='_name',
),
migrations.RemoveField(
model_name='inventoryitemtemplate',
name='_name',
),
migrations.RemoveField(
model_name='modulebay',
name='_name',
),
migrations.RemoveField(
model_name='modulebaytemplate',
name='_name',
),
migrations.RemoveField(
model_name='poweroutlet',
name='_name',
),
migrations.RemoveField(
model_name='poweroutlettemplate',
name='_name',
),
migrations.RemoveField(
model_name='powerport',
name='_name',
),
migrations.RemoveField(
model_name='powerporttemplate',
name='_name',
),
migrations.RemoveField(
model_name='rack',
name='_name',
),
migrations.RemoveField(
model_name='rearport',
name='_name',
),
migrations.RemoveField(
model_name='rearporttemplate',
name='_name',
),
migrations.RemoveField(
model_name='site',
name='_name',
),
migrations.AlterField(
model_name='consoleport',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='consoleporttemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='consoleserverport',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='consoleserverporttemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(blank=True, db_collation='natural_sort', max_length=64, null=True),
),
migrations.AlterField(
model_name='devicebay',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='devicebaytemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='frontport',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='frontporttemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='interface',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='interfacetemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='inventoryitem',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='inventoryitemtemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='modulebay',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='modulebaytemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='poweroutlet',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='powerport',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='powerporttemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='rack',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.AlterField(
model_name='rearport',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='rearporttemplate',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='powerfeed',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.AlterField(
model_name='powerpanel',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.AlterField(
model_name='virtualchassis',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='virtualdevicecontext',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
]

View File

@ -44,12 +44,8 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
max_length=64,
help_text=_(
"{module} is accepted as a substitution for the module bay position when attached to a module type."
)
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
),
db_collation="natural_sort"
)
label = models.CharField(
verbose_name=_('label'),
@ -65,7 +61,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
class Meta:
abstract = True
ordering = ('device_type', '_name')
ordering = ('device_type', 'name')
constraints = (
models.UniqueConstraint(
fields=('device_type', 'name'),
@ -125,7 +121,7 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
class Meta:
abstract = True
ordering = ('device_type', 'module_type', '_name')
ordering = ('device_type', 'module_type', 'name')
constraints = (
models.UniqueConstraint(
fields=('device_type', 'name'),
@ -782,7 +778,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
component_model = InventoryItem
class Meta:
ordering = ('device_type__id', 'parent__id', '_name')
ordering = ('device_type__id', 'parent__id', 'name')
indexes = (
models.Index(fields=('component_type', 'component_id')),
)

View File

@ -50,12 +50,8 @@ class ComponentModel(NetBoxModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
max_length=64,
db_collation="natural_sort"
)
label = models.CharField(
verbose_name=_('label'),
@ -71,7 +67,7 @@ class ComponentModel(NetBoxModel):
class Meta:
abstract = True
ordering = ('device', '_name')
ordering = ('device', 'name')
constraints = (
models.UniqueConstraint(
fields=('device', 'name'),
@ -1301,7 +1297,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
class Meta:
ordering = ('device__id', 'parent__id', '_name')
ordering = ('device__id', 'parent__id', 'name')
indexes = (
models.Index(fields=('component_type', 'component_id')),
)

View File

@ -23,7 +23,7 @@ from netbox.config import ConfigItem
from netbox.models import OrganizationalModel, PrimaryModel
from netbox.models.mixins import WeightMixin
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
from utilities.fields import ColorField, CounterCacheField
from utilities.tracking import TrackingModelMixin
from .device_components import *
from .mixins import RenderConfigMixin
@ -582,13 +582,8 @@ class Device(
verbose_name=_('name'),
max_length=64,
blank=True,
null=True
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True,
null=True
null=True,
db_collation="natural_sort"
)
serial = models.CharField(
max_length=50,
@ -775,7 +770,7 @@ class Device(
)
class Meta:
ordering = ('_name', 'pk') # Name may be null
ordering = ('name', 'pk') # Name may be null
constraints = (
models.UniqueConstraint(
Lower('name'), 'site', 'tenant',
@ -1320,7 +1315,8 @@ class VirtualChassis(PrimaryModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
max_length=64,
db_collation="natural_sort"
)
domain = models.CharField(
verbose_name=_('domain'),
@ -1382,7 +1378,8 @@ class VirtualDeviceContext(PrimaryModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
max_length=64,
db_collation="natural_sort"
)
status = models.CharField(
verbose_name=_('status'),

View File

@ -59,28 +59,24 @@ class CachedScopeMixin(models.Model):
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)
_region = models.ForeignKey(
to='dcim.Region',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)
_site_group = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)

View File

@ -36,7 +36,8 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
prerequisite_models = (
@ -86,7 +87,8 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
status = models.CharField(
verbose_name=_('status'),

View File

@ -19,7 +19,7 @@ from netbox.models.mixins import WeightMixin
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.conversion import to_grams
from utilities.data import array_to_string, drange
from utilities.fields import ColorField, NaturalOrderingField
from utilities.fields import ColorField
from .device_components import PowerPort
from .devices import Device, Module
from .power import PowerFeed
@ -255,12 +255,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
)
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
db_collation="natural_sort"
)
facility_id = models.CharField(
max_length=50,
@ -340,7 +336,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
)
class Meta:
ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique
ordering = ('site', 'location', 'name', 'pk') # (site, location, name) may be non-unique
constraints = (
# Name and facility_id must be unique *only* within a Location
models.UniqueConstraint(

View File

@ -8,7 +8,6 @@ from dcim.choices import *
from dcim.constants import *
from netbox.models import NestedGroupModel, PrimaryModel
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.fields import NaturalOrderingField
__all__ = (
'Location',
@ -143,12 +142,8 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_("Full name of the site")
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
help_text=_("Full name of the site"),
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),
@ -245,7 +240,7 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
)
class Meta:
ordering = ('_name',)
ordering = ('name',)
verbose_name = _('site')
verbose_name_plural = _('sites')

View File

@ -132,7 +132,6 @@ class PlatformTable(NetBoxTable):
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
order_by=('_name',),
template_code=DEVICE_LINK,
linkify=True
)
@ -288,7 +287,6 @@ class DeviceComponentTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True,
order_by=('_name',)
)
device_status = columns.ChoiceFieldColumn(
accessor=tables.A('device__status'),
@ -391,7 +389,6 @@ class DeviceConsolePortTable(ConsolePortTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -433,7 +430,6 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
verbose_name=_('Name'),
template_code='<i class="mdi mdi-console-network-outline"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -482,7 +478,6 @@ class DevicePowerPortTable(PowerPortTable):
verbose_name=_('Name'),
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
'{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -531,7 +526,6 @@ class DevicePowerOutletTable(PowerOutletTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -550,6 +544,11 @@ class DevicePowerOutletTable(PowerOutletTable):
class BaseInterfaceTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True,
order_by=('_name',)
)
enabled = columns.BooleanColumn(
verbose_name=_('Enabled'),
)
@ -597,7 +596,7 @@ class BaseInterfaceTable(NetBoxTable):
return ",".join([str(obj) for obj in value.all()])
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
@ -736,7 +735,6 @@ class DeviceFrontPortTable(FrontPortTable):
verbose_name=_('Name'),
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -783,7 +781,6 @@ class DeviceRearPortTable(RearPortTable):
verbose_name=_('Name'),
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -846,7 +843,6 @@ class DeviceDeviceBayTable(DeviceBayTable):
verbose_name=_('Name'),
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)
actions = columns.ActionsColumn(
@ -915,7 +911,6 @@ class DeviceModuleBayTable(ModuleBayTable):
name = columns.MPTTColumn(
verbose_name=_('Name'),
linkify=True,
order_by=Accessor('_name')
)
actions = columns.ActionsColumn(
extra_buttons=MODULEBAY_BUTTONS
@ -982,7 +977,6 @@ class DeviceInventoryItemTable(InventoryItemTable):
verbose_name=_('Name'),
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
)

View File

@ -163,9 +163,7 @@ class ComponentTemplateTable(NetBoxTable):
id = tables.Column(
verbose_name=_('ID')
)
name = tables.Column(
order_by=('_name',)
)
name = tables.Column()
class Meta(NetBoxTable.Meta):
exclude = ('id', )
@ -220,6 +218,10 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
class InterfaceTemplateTable(ComponentTemplateTable):
name = tables.Column(
verbose_name=_('Name'),
order_by=('_name',)
)
enabled = columns.BooleanColumn(
verbose_name=_('Enabled'),
)

View File

@ -111,7 +111,6 @@ class RackTypeTable(NetBoxTable):
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
order_by=('_name',),
linkify=True
)
location = tables.Column(

View File

@ -688,8 +688,7 @@ class RackElevationListView(generic.ObjectListView):
sort = request.GET.get('sort', 'name')
if sort not in ORDERING_CHOICES:
sort = 'name'
sort_field = sort.replace("name", "_name") # Use natural ordering
racks = racks.order_by(sort_field)
racks = racks.order_by(sort)
# Pagination
per_page = get_paginate_count(request)
@ -731,8 +730,8 @@ class RackView(GetRelatedModelsMixin, generic.ObjectView):
peer_racks = peer_racks.filter(location=instance.location)
else:
peer_racks = peer_racks.filter(location__isnull=True)
next_rack = peer_racks.filter(_name__gt=instance._name).first()
prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first()
next_rack = peer_racks.filter(name__gt=instance.name).first()
prev_rack = peer_racks.filter(name__lt=instance.name).reverse().first()
# Determine any additional parameters to pass when embedding the rack elevations
svg_extra = '&'.join([

View File

@ -223,7 +223,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
from extras.scripts import get_module_and_script
module_name, script_name = action_object.split('.', 1)
try:
module, script = get_module_and_script(module_name, script_name)
script = get_module_and_script(module_name, script_name)[1]
except ObjectDoesNotExist:
raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
self.instance.action_object = script

View File

@ -38,7 +38,7 @@ class Command(BaseCommand):
data = {}
module_name, script_name = script.split('.', 1)
module, script_obj = get_module_and_script(module_name, script_name)
script_obj = get_module_and_script(module_name, script_name)[1]
script = script_obj.python_class
# Take user from command line if provided and exists, other

View File

@ -30,7 +30,7 @@ def get_python_name(scriptmodule):
"""
Return the Python name of a ScriptModule's file on disk.
"""
path, filename = os.path.split(scriptmodule.file_path)
filename = os.path.split(scriptmodule.file_path)[0]
return os.path.splitext(filename)[0]
@ -128,7 +128,7 @@ def update_event_rules(apps, schema_editor):
for eventrule in EventRule.objects.filter(action_object_type=scriptmodule_ct):
name = eventrule.action_parameters.get('script_name')
obj, created = Script.objects.get_or_create(
obj, __ = Script.objects.get_or_create(
module_id=eventrule.action_object_id,
name=name,
defaults={'is_executable': False}

View File

@ -2,8 +2,9 @@ from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.constants import LOCATION_SCOPE_TYPES
from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, PREFIX_SCOPE_TYPES
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
from ipam.models import Aggregate, IPAddress, IPRange, Prefix
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
@ -47,7 +48,7 @@ class PrefixSerializer(NetBoxModelSerializer):
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=PREFIX_SCOPE_TYPES
model__in=LOCATION_SCOPE_TYPES
),
allow_null=True,
required=False,

View File

@ -18,7 +18,7 @@ class IPAMConfig(AppConfig):
# Register denormalized fields
denormalized.register(Prefix, '_site', {
'_region': 'region',
'_sitegroup': 'group',
'_site_group': 'group',
})
denormalized.register(Prefix, '_location', {
'_site': 'site',

View File

@ -23,11 +23,6 @@ VRF_RD_MAX_LENGTH = 21
PREFIX_LENGTH_MIN = 1
PREFIX_LENGTH_MAX = 127 # IPv6
# models values for ContentTypes which may be Prefix scope types
PREFIX_SCOPE_TYPES = (
'region', 'sitegroup', 'site', 'location',
)
#
# IPAddresses

View File

@ -1,5 +1,6 @@
import django_filters
import netaddr
from dcim.base_filtersets import ScopedFilterSet
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
@ -9,7 +10,7 @@ from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError
from circuits.models import Provider
from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
from dcim.models import Device, Interface, Region, Site, SiteGroup
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import (
@ -273,7 +274,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'description', 'weight')
class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
family = django_filters.NumberFilter(
field_name='prefix',
lookup_expr='family'
@ -334,57 +335,6 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='rd',
label=_('VRF (RD)'),
)
scope_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_sitegroup',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_sitegroup',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
vlan_id = django_filters.ModelMultipleChoiceFilter(
queryset=VLAN.objects.all(),
label=_('VLAN (ID)'),

View File

@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from dcim.forms.mixins import ScopedBulkEditForm
from dcim.models import Region, Site, SiteGroup
from ipam.choices import *
from ipam.constants import *
@ -205,20 +206,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('description',)
class PrefixBulkEditForm(NetBoxModelBulkEditForm):
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
required=False,
label=_('Scope type')
)
scope = DynamicModelChoiceField(
label=_('Scope'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
@ -286,20 +274,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
'vlan', 'vrf', 'tenant', 'role', 'scope', 'description', 'comments',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if scope_type_id := get_field_value(self, 'scope_type'):
try:
scope_type = ContentType.objects.get(pk=scope_type_id)
model = scope_type.model_class()
self.fields['scope'].queryset = model.objects.all()
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
self.fields['scope'].disabled = False
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
vrf = DynamicModelChoiceField(

View File

@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
from dcim.forms.mixins import ScopedImportForm
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
@ -154,7 +155,7 @@ class RoleImportForm(NetBoxModelImportForm):
fields = ('name', 'slug', 'weight', 'description', 'tags')
class PrefixImportForm(NetBoxModelImportForm):
class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(),
@ -169,11 +170,6 @@ class PrefixImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned tenant')
)
scope_type = CSVContentTypeField(
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
required=False,
label=_('Scope type (app & model)')
)
vlan_group = CSVModelChoiceField(
label=_('VLAN group'),
queryset=VLANGroup.objects.all(),
@ -208,7 +204,7 @@ class PrefixImportForm(NetBoxModelImportForm):
'mark_utilized', 'description', 'comments', 'tags',
)
labels = {
'scope_id': 'Scope ID',
'scope_id': _('Scope ID'),
}
def __init__(self, data=None, *args, **kwargs):

View File

@ -4,6 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
from dcim.forms.mixins import ScopedForm
from ipam.choices import *
from ipam.constants import *
from ipam.formfields import IPNetworkFormField
@ -197,25 +198,12 @@ class RoleForm(NetBoxModelForm):
]
class PrefixForm(TenancyForm, NetBoxModelForm):
class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('VRF')
)
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
widget=HTMXSelect(),
required=False,
label=_('Scope type')
)
scope = DynamicModelChoiceField(
label=_('Scope'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
@ -248,36 +236,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
'tenant', 'description', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.scope:
initial['scope'] = instance.scope
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if scope_type_id := get_field_value(self, 'scope_type'):
try:
scope_type = ContentType.objects.get(pk=scope_type_id)
model = scope_type.model_class()
self.fields['scope'].queryset = model.objects.all()
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
self.fields['scope'].disabled = False
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
if self.instance and scope_type_id != self.instance.scope_type_id:
self.initial['scope'] = None
def clean(self):
super().clean()
# Assign the selected scope (if any)
self.instance.scope = self.cleaned_data.get('scope')
class IPRangeForm(TenancyForm, NetBoxModelForm):
vrf = DynamicModelChoiceField(

View File

@ -154,7 +154,7 @@ class IPRangeType(NetBoxObjectType):
@strawberry_django.type(
models.Prefix,
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_sitegroup'),
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
filters=PrefixFilter
)
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):

View File

@ -108,8 +108,8 @@ class NetIn(Lookup):
return self.rhs
def as_sql(self, qn, connection):
lhs, lhs_params = self.process_lhs(qn, connection)
rhs, rhs_params = self.process_rhs(qn, connection)
lhs = self.process_lhs(qn, connection)[0]
rhs_params = self.process_rhs(qn, connection)[1]
with_mask, without_mask = [], []
for address in rhs_params[0]:
if '/' in address:

View File

@ -11,11 +11,11 @@ def populate_denormalized_fields(apps, schema_editor):
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site')
for prefix in prefixes:
prefix._region_id = prefix.site.region_id
prefix._sitegroup_id = prefix.site.group_id
prefix._site_group_id = prefix.site.group_id
prefix._site_id = prefix.site_id
# Note: Location cannot be set prior to migration
Prefix.objects.bulk_update(prefixes, ['_region', '_sitegroup', '_site'])
Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site'])
class Migration(migrations.Migration):
@ -29,22 +29,22 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='prefix',
name='_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.location'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'),
),
migrations.AddField(
model_name='prefix',
name='_region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.region'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'),
),
migrations.AddField(
model_name='prefix',
name='_site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.site'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
),
migrations.AddField(
model_name='prefix',
name='_sitegroup',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.sitegroup'),
name='_site_group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'),
),
# Populate denormalized FK values

View File

@ -0,0 +1,32 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0075_vlan_qinq'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='asnrange',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='routetarget',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=21, unique=True),
),
migrations.AlterField(
model_name='vlangroup',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.AlterField(
model_name='vrf',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
]

View File

@ -16,7 +16,8 @@ class ASNRange(OrganizationalModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),

View File

@ -1,5 +1,4 @@
import netaddr
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
@ -9,6 +8,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from core.models import ObjectType
from dcim.models.mixins import CachedScopeMixin
from ipam.choices import *
from ipam.constants import *
from ipam.fields import IPNetworkField, IPAddressField
@ -198,7 +198,7 @@ class Role(OrganizationalModel):
return self.name
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
@ -208,22 +208,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
verbose_name=_('prefix'),
help_text=_('IPv4 or IPv6 network with mask')
)
scope_type = models.ForeignKey(
to='contenttypes.ContentType',
on_delete=models.PROTECT,
limit_choices_to=Q(model__in=PREFIX_SCOPE_TYPES),
related_name='+',
blank=True,
null=True
)
scope_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
scope = GenericForeignKey(
ct_field='scope_type',
fk_field='scope_id'
)
vrf = models.ForeignKey(
to='ipam.VRF',
on_delete=models.PROTECT,
@ -272,36 +256,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
help_text=_("Treat as fully utilized")
)
# Cached associations to enable efficient filtering
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
_region = models.ForeignKey(
to='dcim.Region',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
_sitegroup = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
# Cached depth & child counts
_depth = models.PositiveSmallIntegerField(
default=0,
@ -368,25 +322,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
super().save(*args, **kwargs)
def cache_related_objects(self):
self._region = self._sitegroup = self._site = self._location = None
if self.scope_type:
scope_type = self.scope_type.model_class()
if scope_type == apps.get_model('dcim', 'region'):
self._region = self.scope
elif scope_type == apps.get_model('dcim', 'sitegroup'):
self._sitegroup = self.scope
elif scope_type == apps.get_model('dcim', 'site'):
self._region = self.scope.region
self._sitegroup = self.scope.group
self._site = self.scope
elif scope_type == apps.get_model('dcim', 'location'):
self._region = self.scope.site.region
self._sitegroup = self.scope.site.group
self._site = self.scope.site
self._location = self.scope
cache_related_objects.alters_data = True
@property
def family(self):
return self.prefix.version if self.prefix else None

View File

@ -35,7 +35,8 @@ class VLANGroup(OrganizationalModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),

View File

@ -18,7 +18,8 @@ class VRF(PrimaryModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
rd = models.CharField(
max_length=VRF_RD_MAX_LENGTH,
@ -74,7 +75,8 @@ class RouteTarget(PrimaryModel):
verbose_name=_('name'),
max_length=VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4)
unique=True,
help_text=_('Route target value (formatted in accordance with RFC 4360)')
help_text=_('Route target value (formatted in accordance with RFC 4360)'),
db_collation="natural_sort"
)
tenant = models.ForeignKey(
to='tenancy.Tenant',

View File

@ -42,7 +42,7 @@ class PrefixOrderingTestCase(OrderingTestBase):
"""
This is a very basic test, which tests both prefixes without VRFs and prefixes with VRFs
"""
vrf1, vrf2, vrf3 = list(VRF.objects.all())
vrf1, vrf2 = VRF.objects.all()[:2]
prefixes = (
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/24')),
@ -106,7 +106,7 @@ class PrefixOrderingTestCase(OrderingTestBase):
VRF A:10.1.1.0/24
None: 192.168.0.0/16
"""
vrf1, vrf2, vrf3 = list(VRF.objects.all())
vrf1 = VRF.objects.first()
prefixes = [
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/8')),
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/16')),
@ -130,7 +130,7 @@ class IPAddressOrderingTestCase(OrderingTestBase):
"""
This function tests ordering with the inclusion of vrfs
"""
vrf1, vrf2, vrf3 = list(VRF.objects.all())
vrf1, vrf2 = VRF.objects.all()[:2]
addresses = (
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrf1, address=netaddr.IPNetwork('10.0.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrf1, address=netaddr.IPNetwork('10.0.1.1/24')),

View File

@ -107,7 +107,7 @@ class ObjectPermissionMixin:
return perms
def has_perm(self, user_obj, perm, obj=None):
app_label, action, model_name = resolve_permission(perm)
app_label, __, model_name = resolve_permission(perm)
# Superusers implicitly have all permissions
if user_obj.is_active and user_obj.is_superuser:

View File

@ -213,7 +213,6 @@ class PluginTest(TestCase):
self.assertEqual(get_plugin_config(plugin, 'bar'), None)
self.assertEqual(get_plugin_config(plugin, 'bar', default=456), 456)
def test_events_pipeline(self):
"""
Check that events pipeline is registered.

View File

@ -49,7 +49,7 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
template = loader.get_template(template_name)
except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
type_, error, traceback = sys.exc_info()
type_, error = sys.exc_info()[:2]
return HttpResponseServerError(template.render({
'error': error,

View File

@ -22,6 +22,14 @@
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Scope" %}</th>
{% if object.scope %}
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
{% else %}
<td>{{ ''|placeholder }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>

View File

@ -0,0 +1,27 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0016_charfield_null_choices'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='contact',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.AlterField(
model_name='tenant',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.AlterField(
model_name='tenantgroup',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
]

View File

@ -56,7 +56,8 @@ class Contact(PrimaryModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
title = models.CharField(
verbose_name=_('title'),

View File

@ -18,7 +18,8 @@ class TenantGroup(NestedGroupModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),
@ -39,7 +40,8 @@ class Tenant(ContactsMixin, PrimaryModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),

View File

@ -129,7 +129,7 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None):
for field_name, field in serializer_class._declared_fields.items():
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
related_field = model._meta.get_field(field.relation).field
related_field = getattr(model, field.relation).field
annotations[field_name] = count_related(related_field.model, related_field.name)
return annotations

View File

@ -49,7 +49,7 @@ def handle_rest_api_exception(request, *args, **kwargs):
"""
Handle exceptions and return a useful error message for REST API requests.
"""
type_, error, traceback = sys.exc_info()
type_, error = sys.exc_info()[:2]
data = {
'error': str(error),
'exception': type_.__name__,

View File

@ -5,7 +5,6 @@ from django.db import models
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from utilities.ordering import naturalize
from .forms.widgets import ColorSelect
from .validators import ColorValidator
@ -40,7 +39,7 @@ class NaturalOrderingField(models.CharField):
"""
description = "Stores a representation of its target field suitable for natural ordering"
def __init__(self, target_field, naturalize_function=naturalize, *args, **kwargs):
def __init__(self, target_field, naturalize_function, *args, **kwargs):
self.target_field = target_field
self.naturalize_function = naturalize_function
super().__init__(*args, **kwargs)

View File

@ -83,7 +83,7 @@ class CountersTest(TestCase):
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_mptt_child_delete(self):
device1, device2 = Device.objects.all()
device1 = Device.objects.first()
inventory_item1 = InventoryItem.objects.create(device=device1, name='Inventory Item 1')
InventoryItem.objects.create(device=device1, name='Inventory Item 2', parent=inventory_item1)
device1.refresh_from_db()

View File

@ -59,6 +59,14 @@ class ClusterSerializer(NetBoxModelSerializer):
)
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True)
allocated_vcpus = serializers.DecimalField(
read_only=True,
max_digits=8,
decimal_places=2,
)
allocated_memory = serializers.IntegerField(read_only=True)
allocated_disk = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('devices')
@ -69,7 +77,7 @@ class ClusterSerializer(NetBoxModelSerializer):
fields = [
'id', 'url', 'display_url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'scope_id', 'scope',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'virtualmachine_count',
'virtualmachine_count', 'allocated_vcpus', 'allocated_memory', 'allocated_disk'
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count')
@ -80,5 +88,3 @@ class ClusterSerializer(NetBoxModelSerializer):
serializer = get_serializer_for_model(obj.scope)
context = {'request': self.context['request']}
return serializer(obj.scope, nested=True, context=context).data

View File

@ -1,3 +1,4 @@
from django.db.models import Sum
from rest_framework.routers import APIRootView
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
@ -33,7 +34,11 @@ class ClusterGroupViewSet(NetBoxModelViewSet):
class ClusterViewSet(NetBoxModelViewSet):
queryset = Cluster.objects.all()
queryset = Cluster.objects.prefetch_related('virtual_machines').annotate(
allocated_vcpus=Sum('virtual_machines__vcpus'),
allocated_memory=Sum('virtual_machines__memory'),
allocated_disk=Sum('virtual_machines__disk'),
)
serializer_class = serializers.ClusterSerializer
filterset_class = filtersets.ClusterFilterSet

View File

@ -2,7 +2,8 @@ import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.filtersets import CommonInterfaceFilterSet, ScopedFilterSet
from dcim.filtersets import CommonInterfaceFilterSet
from dcim.base_filtersets import ScopedFilterSet
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate

View File

@ -25,7 +25,6 @@ class ComponentType(NetBoxObjectType):
"""
Base type for device/VM components
"""
_name: str
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]
@ -77,7 +76,6 @@ class ClusterTypeType(OrganizationalObjectType):
filters=VirtualMachineFilter
)
class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
_name: str
interface_count: BigInt
virtual_disk_count: BigInt
interface_count: BigInt
@ -102,6 +100,7 @@ class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
filters=VMInterfaceFilter
)
class VMInterfaceType(IPAddressesMixin, ComponentType):
_name: str
mac_address: str | None
parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.9 on 2024-11-14 19:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0196_qinq_svlan'),
('virtualization', '0045_clusters_cached_relations'),
]
operations = [
migrations.AlterField(
model_name='cluster',
name='_location',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'
),
),
migrations.AlterField(
model_name='cluster',
name='_region',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'
),
),
migrations.AlterField(
model_name='cluster',
name='_site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
),
migrations.AlterField(
model_name='cluster',
name='_site_group',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'
),
),
]

View File

@ -0,0 +1,43 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0046_alter_cluster__location_alter_cluster__region_and_more'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterModelOptions(
name='virtualmachine',
options={'ordering': ('name', 'pk')},
),
migrations.AlterModelOptions(
name='virtualdisk',
options={'ordering': ('virtual_machine', 'name')},
),
migrations.RemoveField(
model_name='virtualmachine',
name='_name',
),
migrations.AlterField(
model_name='virtualdisk',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='virtualmachine',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=64),
),
migrations.AlterField(
model_name='cluster',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
migrations.RemoveField(
model_name='virtualdisk',
name='_name',
),
]

View File

@ -50,7 +50,8 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
type = models.ForeignKey(
verbose_name=_('type'),

View File

@ -69,12 +69,8 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
)
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
max_length=64,
db_collation="natural_sort"
)
status = models.CharField(
max_length=50,
@ -152,7 +148,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
)
class Meta:
ordering = ('_name', 'pk') # Name may be non-unique
ordering = ('name', 'pk') # Name may be non-unique
constraints = (
models.UniqueConstraint(
Lower('name'), 'cluster', 'tenant',
@ -273,13 +269,8 @@ class ComponentModel(NetBoxModel):
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
)
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
max_length=64,
db_collation="natural_sort"
)
description = models.CharField(
verbose_name=_('description'),
@ -289,7 +280,6 @@ class ComponentModel(NetBoxModel):
class Meta:
abstract = True
ordering = ('virtual_machine', CollateAsChar('_name'))
constraints = (
models.UniqueConstraint(
fields=('virtual_machine', 'name'),
@ -311,10 +301,9 @@ class ComponentModel(NetBoxModel):
class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces' # Override ComponentModel
name = models.CharField(
verbose_name=_('name'),
max_length=64,
)
_name = NaturalOrderingField(
target_field='name',
@ -322,6 +311,11 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
max_length=100,
blank=True
)
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='interfaces' # Override ComponentModel
)
ip_addresses = GenericRelation(
to='ipam.IPAddress',
content_type_field='assigned_object_type',
@ -358,6 +352,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
class Meta(ComponentModel.Meta):
verbose_name = _('interface')
verbose_name_plural = _('interfaces')
ordering = ('virtual_machine', CollateAsChar('_name'))
def clean(self):
super().clean()
@ -416,3 +411,4 @@ class VirtualDisk(ComponentModel, TrackingModelMixin):
class Meta(ComponentModel.Meta):
verbose_name = _('virtual disk')
verbose_name_plural = _('virtual disks')
ordering = ('virtual_machine', 'name')

View File

@ -53,7 +53,6 @@ VMINTERFACE_BUTTONS = """
class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
order_by=('_name',),
linkify=True
)
status = columns.ChoiceFieldColumn(

View File

@ -0,0 +1,47 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vpn', '0006_charfield_null_choices'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='ikepolicy',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='ikeproposal',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='ipsecpolicy',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='ipsecprofile',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='ipsecproposal',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='l2vpn',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='tunnel',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
]

View File

@ -22,7 +22,8 @@ class IKEProposal(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
authentication_method = models.CharField(
verbose_name=('authentication method'),
@ -67,7 +68,8 @@ class IKEPolicy(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
version = models.PositiveSmallIntegerField(
verbose_name=_('version'),
@ -125,7 +127,8 @@ class IPSecProposal(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
encryption_algorithm = models.CharField(
verbose_name=_('encryption'),
@ -176,7 +179,8 @@ class IPSecPolicy(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
proposals = models.ManyToManyField(
to='vpn.IPSecProposal',
@ -211,7 +215,8 @@ class IPSecProfile(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
mode = models.CharField(
verbose_name=_('mode'),

View File

@ -20,7 +20,8 @@ class L2VPN(ContactsMixin, PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),

View File

@ -31,7 +31,8 @@ class Tunnel(PrimaryModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
status = models.CharField(
verbose_name=_('status'),

View File

@ -1,9 +1,13 @@
from rest_framework import serializers
from dcim.constants import LOCATION_SCOPE_TYPES
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from ipam.api.serializers_.vlans import VLANSerializer
from netbox.api.fields import ChoiceField
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
from wireless.choices import *
from wireless.models import WirelessLAN, WirelessLANGroup
from .nested import NestedWirelessLANGroupSerializer
@ -34,12 +38,30 @@ class WirelessLANSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=LOCATION_SCOPE_TYPES
),
allow_null=True,
required=False,
default=None
)
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True)
class Meta:
model = WirelessLAN
fields = [
'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant',
'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields',
'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'scope_type', 'scope_id', 'scope',
'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'ssid', 'description')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_scope(self, obj):
if obj.scope_id is None:
return None
serializer = get_serializer_for_model(obj.scope)
context = {'request': self.context['request']}
return serializer(obj.scope, nested=True, context=context).data

View File

@ -2,6 +2,7 @@ import django_filters
from django.db.models import Q
from dcim.choices import LinkStatusChoices
from dcim.base_filtersets import ScopedFilterSet
from dcim.models import Interface
from ipam.models import VLAN
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
@ -43,7 +44,7 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'description')
class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
group_id = TreeNodeMultipleChoiceFilter(
queryset=WirelessLANGroup.objects.all(),
field_name='group',
@ -74,7 +75,7 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta:
model = WirelessLAN
fields = ('id', 'ssid', 'auth_psk', 'description')
fields = ('id', 'ssid', 'auth_psk', 'scope_id', 'description')
def search(self, queryset, name, value):
if not value.strip():

View File

@ -2,6 +2,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.choices import LinkStatusChoices
from dcim.forms.mixins import ScopedBulkEditForm
from ipam.models import VLAN
from netbox.choices import *
from netbox.forms import NetBoxModelBulkEditForm
@ -39,7 +40,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('parent', 'description')
class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(WirelessLANStatusChoices),
@ -89,10 +90,11 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
model = WirelessLAN
fieldsets = (
FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
FieldSet('scope_type', 'scope', name=_('Scope')),
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
)
nullable_fields = (
'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'scope', 'comments',
)

View File

@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _
from dcim.choices import LinkStatusChoices
from dcim.forms.mixins import ScopedImportForm
from dcim.models import Interface
from ipam.models import VLAN
from netbox.choices import *
@ -32,7 +33,7 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm):
fields = ('name', 'slug', 'parent', 'description', 'tags')
class WirelessLANImportForm(NetBoxModelImportForm):
class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm):
group = CSVModelChoiceField(
label=_('Group'),
queryset=WirelessLANGroup.objects.all(),
@ -75,9 +76,12 @@ class WirelessLANImportForm(NetBoxModelImportForm):
class Meta:
model = WirelessLAN
fields = (
'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
'comments', 'tags',
'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'scope_type', 'scope_id',
'description', 'comments', 'tags',
)
labels = {
'scope_id': _('Scope ID'),
}
class WirelessLinkImportForm(NetBoxModelImportForm):

View File

@ -2,6 +2,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.choices import LinkStatusChoices
from dcim.models import Location, Region, Site, SiteGroup
from netbox.choices import *
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
@ -33,6 +34,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
)
@ -65,6 +67,31 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Pre-shared key'),
required=False
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
null_option='None',
query_params={
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Location')
)
tag = TagFilterField(model)

View File

@ -2,6 +2,7 @@ from django.forms import PasswordInput
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Location, Site
from dcim.forms.mixins import ScopedForm
from ipam.models import VLAN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
@ -35,7 +36,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
]
class WirelessLANForm(TenancyForm, NetBoxModelForm):
class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm):
group = DynamicModelChoiceField(
label=_('Group'),
queryset=WirelessLANGroup.objects.all(),
@ -51,6 +52,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
fieldsets = (
FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
FieldSet('scope_type', 'scope', name=_('Scope')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
)
@ -59,7 +61,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
model = WirelessLAN
fields = [
'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
'description', 'comments', 'tags',
'scope_type', 'description', 'comments', 'tags',
]
widgets = {
'auth_psk': PasswordInput(

View File

@ -1,4 +1,4 @@
from typing import Annotated, List
from typing import Annotated, List, Union
import strawberry
import strawberry_django
@ -28,7 +28,7 @@ class WirelessLANGroupType(OrganizationalObjectType):
@strawberry_django.type(
models.WirelessLAN,
fields='__all__',
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
filters=WirelessLANFilter
)
class WirelessLANType(NetBoxObjectType):
@ -38,6 +38,15 @@ class WirelessLANType(NetBoxObjectType):
interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.field
def scope(self) -> Annotated[Union[
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
], strawberry.union("WirelessLANScopeType")] | None:
return self.scope
@strawberry_django.type(
models.WirelessLink,

View File

@ -0,0 +1,77 @@
# Generated by Django 5.0.9 on 2024-11-04 16:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0196_qinq_svlan'),
('wireless', '0010_charfield_null_choices'),
]
operations = [
migrations.AddField(
model_name='wirelesslan',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='_%(class)ss',
to='dcim.location',
),
),
migrations.AddField(
model_name='wirelesslan',
name='_region',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='_%(class)ss',
to='dcim.region',
),
),
migrations.AddField(
model_name='wirelesslan',
name='_site',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='_%(class)ss',
to='dcim.site',
),
),
migrations.AddField(
model_name='wirelesslan',
name='_site_group',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='_%(class)ss',
to='dcim.sitegroup',
),
),
migrations.AddField(
model_name='wirelesslan',
name='scope_id',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='wirelesslan',
name='scope_type',
field=models.ForeignKey(
blank=True,
limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location'))),
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype',
),
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.9 on 2024-11-14 19:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0196_qinq_svlan'),
('wireless', '0011_wirelesslan__location_wirelesslan__region_and_more'),
]
operations = [
migrations.AlterField(
model_name='wirelesslan',
name='_location',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'
),
),
migrations.AlterField(
model_name='wirelesslan',
name='_region',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'
),
),
migrations.AlterField(
model_name='wirelesslan',
name='_site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
),
migrations.AlterField(
model_name='wirelesslan',
name='_site_group',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'
),
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wireless', '0012_alter_wirelesslan__location_and_more'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='wirelesslangroup',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
]

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import LinkStatusChoices
from dcim.constants import WIRELESS_IFACE_TYPES
from dcim.models.mixins import CachedScopeMixin
from netbox.models import NestedGroupModel, PrimaryModel
from netbox.models.mixins import DistanceMixin
from .choices import *
@ -51,7 +52,8 @@ class WirelessLANGroup(NestedGroupModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
unique=True,
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),
@ -71,7 +73,7 @@ class WirelessLANGroup(NestedGroupModel):
verbose_name_plural = _('wireless LAN groups')
class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
class WirelessLAN(WirelessAuthenticationBase, CachedScopeMixin, PrimaryModel):
"""
A wireless network formed among an arbitrary number of access point and clients.
"""
@ -107,7 +109,7 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
null=True
)
clone_fields = ('ssid', 'group', 'tenant', 'description')
clone_fields = ('ssid', 'group', 'scope_type', 'scope_id', 'tenant', 'description')
class Meta:
ordering = ('ssid', 'pk')

View File

@ -51,6 +51,13 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
scope_type = columns.ContentTypeColumn(
verbose_name=_('Scope Type'),
)
scope = tables.Column(
verbose_name=_('Scope'),
linkify=True
)
interface_count = tables.Column(
verbose_name=_('Interfaces')
)
@ -65,7 +72,7 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
model = WirelessLAN
fields = (
'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type',
'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'created', 'last_updated',
'auth_cipher', 'auth_psk', 'scope', 'scope_type', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'ssid', 'group', 'status', 'description', 'vlan', 'auth_type', 'interface_count')

View File

@ -1,7 +1,7 @@
from django.urls import reverse
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
from dcim.models import Interface, Site
from tenancy.models import Tenant
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from wireless.choices import *
@ -53,6 +53,12 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'),
@ -94,6 +100,8 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
'status': WirelessLANStatusChoices.STATUS_DISABLED,
'tenant': tenants[0].pk,
'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
'scope_type': 'dcim.site',
'scope_id': sites[1].pk,
},
]

View File

@ -1,7 +1,7 @@
from django.test import TestCase
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
from dcim.models import Interface
from dcim.models import Interface, Location, Region, Site, SiteGroup
from ipam.models import VLAN
from netbox.choices import DistanceUnitChoices
from tenancy.models import Tenant
@ -110,6 +110,36 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
)
VLAN.objects.bulk_create(vlans)
regions = (
Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'),
Region(name='Test Region 3', slug='test-region-3'),
)
for r in regions:
r.save()
site_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for site_group in site_groups:
site_group.save()
sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
)
Site.objects.bulk_create(sites)
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[2]),
)
for location in locations:
location.save()
tenants = (
Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'),
@ -127,7 +157,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
auth_psk='PSK1',
description='foobar1'
description='foobar1',
scope=sites[0]
),
WirelessLAN(
ssid='WLAN2',
@ -138,7 +169,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
auth_psk='PSK2',
description='foobar2'
description='foobar2',
scope=locations[0]
),
WirelessLAN(
ssid='WLAN3',
@ -149,12 +181,14 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
auth_psk='PSK3',
description='foobar3'
description='foobar3',
scope=locations[1]
),
)
WirelessLAN.objects.bulk_create(wireless_lans)
for wireless_lan in wireless_lans:
wireless_lan.save()
device = create_test_device('Device 1')
device = create_test_device('Device 1', site=sites[0])
interfaces = (
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_80211N),
@ -217,6 +251,38 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_location(self):
locations = Location.objects.all()[:1]
params = {'location_id': [locations[0].pk,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'location': [locations[0].slug,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_scope_type(self):
params = {'scope_type': 'dcim.location'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = WirelessLink.objects.all()

View File

@ -1,7 +1,8 @@
from django.contrib.contenttypes.models import ContentType
from wireless.choices import *
from wireless.models import *
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
from dcim.models import Interface
from dcim.models import Interface, Site
from netbox.choices import DistanceUnitChoices
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device
@ -56,6 +57,12 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'),
@ -98,15 +105,17 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'ssid': 'WLAN2',
'group': groups[1].pk,
'status': WirelessLANStatusChoices.STATUS_DISABLED,
'scope_type': ContentType.objects.get_for_model(Site).pk,
'scope': sites[1].pk,
'tenant': tenants[1].pk,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"group,ssid,status,tenant",
f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name}",
f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name}",
f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name}",
"group,ssid,status,tenant,scope_type,scope_id",
f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name},,",
f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name},dcim.site,{sites[0].pk}",
f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name},dcim.site,{sites[1].pk}",
)
cls.csv_update_data = (

View File

@ -1,2 +1,4 @@
[lint]
extend-select = ["E1", "E2", "E3", "W"]
ignore = ["E501", "F403", "F405"]
preview = true