mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-13 20:37:44 +01:00
Compare commits
3 Commits
main
...
21364-swag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb90b654cd | ||
|
|
fbd74d3b2c | ||
|
|
a2f31b1094 |
@@ -9,7 +9,7 @@ from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
@@ -99,13 +99,11 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
@@ -129,13 +127,11 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
@@ -167,26 +163,22 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
@@ -197,19 +189,16 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=CircuitType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
@@ -256,12 +245,10 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
)
|
||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Termination A (ID)'),
|
||||
)
|
||||
termination_z_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitTermination.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Termination A (ID)'),
|
||||
)
|
||||
|
||||
@@ -292,10 +279,9 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Circuit.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Circuit'),
|
||||
)
|
||||
termination_type = MultiValueContentTypeFilter()
|
||||
termination_type = ContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
@@ -324,14 +310,12 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
@@ -350,20 +334,17 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
distinct=False,
|
||||
field_name='_provider_network',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider_id',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
@@ -400,7 +381,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
member_type = MultiValueContentTypeFilter()
|
||||
member_type = ContentTypeFilter()
|
||||
circuit = MultiValueCharFilter(
|
||||
method='filter_circuit',
|
||||
field_name='cid',
|
||||
@@ -433,13 +414,11 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Circuit group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Circuit group (slug)'),
|
||||
)
|
||||
@@ -509,49 +488,41 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Virtual circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -577,49 +548,41 @@ class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual circuit'),
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account__account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
distinct=False,
|
||||
field_name='virtual_circuit__provider_network',
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
distinct=False,
|
||||
field_name='interface',
|
||||
label=_('Interface (ID)'),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.filters import MultiValueContentTypeFilter
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .choices import *
|
||||
from .models import *
|
||||
@@ -25,17 +25,14 @@ __all__ = (
|
||||
class DataSourceFilterSet(PrimaryModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=get_data_backend_choices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=DataSourceStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
sync_interval = django_filters.MultipleChoiceFilter(
|
||||
choices=JobIntervalChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -60,13 +57,11 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data source (ID)'),
|
||||
)
|
||||
source = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='source__name',
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Data source (name)'),
|
||||
)
|
||||
@@ -91,10 +86,9 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.with_feature('jobs'),
|
||||
distinct=False,
|
||||
field_name='object_type_id',
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter()
|
||||
object_type = ContentTypeFilter()
|
||||
created = django_filters.DateTimeFilter()
|
||||
created__before = django_filters.DateTimeFilter(
|
||||
field_name='created',
|
||||
@@ -133,7 +127,6 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=JobStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
queue_name = django_filters.CharFilter()
|
||||
@@ -187,21 +180,18 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
label=_('Search'),
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = MultiValueContentTypeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all(),
|
||||
distinct=False,
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
related_object_type = MultiValueContentTypeFilter()
|
||||
related_object_type = ContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User name'),
|
||||
)
|
||||
|
||||
@@ -89,7 +89,6 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
|
||||
with storage.open(self.full_path, 'wb+') as new_file:
|
||||
new_file.write(self.data_file.data)
|
||||
sync_data.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def storage(self):
|
||||
|
||||
@@ -216,7 +216,6 @@ class Job(models.Model):
|
||||
|
||||
# Send signal
|
||||
job_start.send(self)
|
||||
start.alters_data = True
|
||||
|
||||
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
|
||||
"""
|
||||
@@ -246,7 +245,6 @@ class Job(models.Model):
|
||||
|
||||
# Send signal
|
||||
job_end.send(self)
|
||||
terminate.alters_data = True
|
||||
|
||||
def log(self, record: logging.LogRecord):
|
||||
"""
|
||||
|
||||
@@ -209,28 +209,22 @@ def handle_deleted_object(sender, instance, **kwargs):
|
||||
# for the forward direction of the relationship, ensuring that the change is recorded.
|
||||
# Similarly, for many-to-one relationships, we set the value on the related object to None
|
||||
# and save it to trigger a change record on that object.
|
||||
#
|
||||
# Skip this for private models (e.g. CablePath) whose lifecycle is an internal
|
||||
# implementation detail. Django's on_delete handlers (e.g. SET_NULL) already take
|
||||
# care of the database integrity; recording changelog entries for the related
|
||||
# objects would be spurious. (Ref: #21390)
|
||||
if not getattr(instance, '_netbox_private', False):
|
||||
for relation in instance._meta.related_objects:
|
||||
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
if not issubclass(related_model, ChangeLoggingMixin):
|
||||
# We only care about triggering the m2m_changed signal for models which support
|
||||
# change logging
|
||||
continue
|
||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
if type(relation) is ManyToManyRel:
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
|
||||
setattr(obj, related_field_name, None)
|
||||
obj.save()
|
||||
for relation in instance._meta.related_objects:
|
||||
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
if not issubclass(related_model, ChangeLoggingMixin):
|
||||
# We only care about triggering the m2m_changed signal for models which support
|
||||
# change logging
|
||||
continue
|
||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
if type(relation) is ManyToManyRel:
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
|
||||
setattr(obj, related_field_name, None)
|
||||
obj.save()
|
||||
|
||||
# Enqueue the object for event processing
|
||||
queue = events_queue.get()
|
||||
|
||||
@@ -237,9 +237,9 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_changed_object_type(self):
|
||||
params = {'changed_object_type': ['dcim.site']}
|
||||
params = {'changed_object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'changed_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]}
|
||||
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import django_filters
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
@@ -14,7 +14,7 @@ class ScopedFilterSet(BaseFilterSet):
|
||||
"""
|
||||
Provides additional filtering functionality for location, site, etc.. for Scoped models.
|
||||
"""
|
||||
scope_type = MultiValueContentTypeFilter()
|
||||
scope_type = ContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
@@ -43,14 +43,12 @@ class ScopedFilterSet(BaseFilterSet):
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-13 13:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0225_gfk_indexes'),
|
||||
('extras', '0134_owner'),
|
||||
('tenancy', '0022_add_comments_to_organizationalmodel'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='devicerole',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_devicerole_tree_id_lfbf11'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='inventoryitem',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_inventoryitem_tree_id975c'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='inventoryitemtemplate',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_inventoryitemtemplatedee0'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='location',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_location_tree_id_lft_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='modulebay',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_modulebay_tree_id_lft_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='platform',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_platform_tree_id_lft_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='region',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_region_tree_id_lft_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='sitegroup',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='dcim_sitegroup_tree_id_lft_idx'),
|
||||
),
|
||||
]
|
||||
@@ -657,16 +657,6 @@ class CablePath(models.Model):
|
||||
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||
origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Mirror save() - clear _path on origins to prevent stale references
|
||||
# in table views that render _path.destinations
|
||||
if self.path:
|
||||
origin_model = self.origin_type.model_class()
|
||||
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||
origin_model.objects.filter(pk__in=origin_ids, _path=self.pk).update(_path=None)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def origin_type(self):
|
||||
if self.path:
|
||||
|
||||
@@ -1263,9 +1263,6 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
|
||||
clone_fields = ('device',)
|
||||
|
||||
class Meta(ModularComponentModel.Meta):
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('device', 'module', 'name'),
|
||||
|
||||
@@ -401,9 +401,6 @@ class DeviceRole(NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
@@ -455,9 +452,6 @@ class Platform(NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
verbose_name = _('platform')
|
||||
verbose_name_plural = _('platforms')
|
||||
constraints = (
|
||||
|
||||
@@ -44,9 +44,6 @@ class Region(ContactsMixin, NestedGroupModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
@@ -103,9 +100,6 @@ class SiteGroup(ContactsMixin, NestedGroupModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
@@ -324,9 +318,6 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('site', 'parent', 'name'),
|
||||
|
||||
@@ -170,8 +170,6 @@ def nullify_connected_endpoints(instance, **kwargs):
|
||||
# Remove the deleted CableTermination if it's one of the path's originating nodes
|
||||
if instance.termination in cablepath.origins:
|
||||
cablepath.origins.remove(instance.termination)
|
||||
# Clear _path on the removed origin to prevent stale connection display
|
||||
model.objects.filter(pk=instance.termination_id, _path=cablepath.pk).update(_path=None)
|
||||
cablepath.retrace()
|
||||
|
||||
|
||||
|
||||
@@ -2806,6 +2806,7 @@ class LegacyCablePathTests(CablePathTestCase):
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||
|
||||
# Create cables 1
|
||||
cable1 = Cable(
|
||||
a_terminations=[interface1],
|
||||
b_terminations=[interface2, interface3]
|
||||
@@ -2837,10 +2838,6 @@ class LegacyCablePathTests(CablePathTestCase):
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Verify _path is cleared on removed interface (#21127)
|
||||
interface3.refresh_from_db()
|
||||
self.assertPathIsNotSet(interface3)
|
||||
|
||||
def test_401_exclude_midspan_devices(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1][Test Device][RP1] --C2-- [RP2][Test Device][FP2] --C3-- [IF2]
|
||||
|
||||
@@ -6251,7 +6251,7 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_component_type(self):
|
||||
params = {'component_type': ['dcim.interface']}
|
||||
params = {'component_type': 'dcim.interface'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_status(self):
|
||||
@@ -6723,8 +6723,10 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_termination_types(self):
|
||||
params = {'termination_a_type': ['dcim.consoleport', 'dcim.consoleserverport']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'termination_a_type': 'dcim.consoleport'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
# params = {'termination_b_type': 'dcim.consoleserverport'}
|
||||
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_termination_ids(self):
|
||||
interface_ids = CableTermination.objects.filter(
|
||||
@@ -6732,7 +6734,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
cable_end='A'
|
||||
).values_list('termination_id', flat=True)
|
||||
params = {
|
||||
'termination_a_type': ['dcim.interface'],
|
||||
'termination_a_type': 'dcim.interface',
|
||||
'termination_a_id': list(interface_ids),
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_rq.queues import get_connection
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiExample
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
||||
from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rq import Worker
|
||||
|
||||
from extras import filtersets
|
||||
@@ -264,10 +264,57 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
|
||||
#
|
||||
|
||||
@extend_schema_view(
|
||||
update=extend_schema(request=serializers.ScriptInputSerializer),
|
||||
partial_update=extend_schema(request=serializers.ScriptInputSerializer),
|
||||
create=extend_schema(exclude=True), # Hide POST from list endpoint in Swagger
|
||||
update=extend_schema(
|
||||
request=serializers.ScriptInputSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Script with no variables',
|
||||
value={'data': {}, 'commit': True},
|
||||
request_only=True,
|
||||
),
|
||||
OpenApiExample(
|
||||
'Script with variables',
|
||||
value={
|
||||
'data': {
|
||||
'variable_name': 'example_value',
|
||||
'another_variable': 123
|
||||
},
|
||||
'commit': True
|
||||
},
|
||||
request_only=True,
|
||||
),
|
||||
]
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
request=serializers.ScriptInputSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Script with no variables',
|
||||
value={'data': {}, 'commit': True},
|
||||
request_only=True,
|
||||
),
|
||||
OpenApiExample(
|
||||
'Script with variables',
|
||||
value={
|
||||
'data': {
|
||||
'variable_name': 'example_value',
|
||||
'another_variable': 123
|
||||
},
|
||||
'commit': True
|
||||
},
|
||||
request_only=True,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
class ScriptViewSet(ModelViewSet):
|
||||
class ScriptViewSet(
|
||||
ListModelMixin,
|
||||
RetrieveModelMixin,
|
||||
UpdateModelMixin,
|
||||
DestroyModelMixin,
|
||||
GenericViewSet
|
||||
):
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
queryset = Script.objects.all()
|
||||
serializer_class = serializers.ScriptSerializer
|
||||
@@ -303,11 +350,32 @@ class ScriptViewSet(ModelViewSet):
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
request=serializers.ScriptInputSerializer,
|
||||
responses={200: serializers.ScriptDetailSerializer},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Script with no variables',
|
||||
value={'data': {}, 'commit': True},
|
||||
request_only=True,
|
||||
),
|
||||
OpenApiExample(
|
||||
'Script with variables',
|
||||
value={
|
||||
'data': {
|
||||
'variable_name': 'example_value',
|
||||
'another_variable': 123
|
||||
},
|
||||
'commit': True
|
||||
},
|
||||
request_only=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
def post(self, request, pk):
|
||||
"""
|
||||
Run a Script identified by its numeric PK or module & name and return the pending Job as the result
|
||||
"""
|
||||
|
||||
script = self._get_script(pk)
|
||||
|
||||
if not request.user.has_perm('extras.run_script', obj=script):
|
||||
|
||||
@@ -10,7 +10,7 @@ from tenancy.models import Tenant, TenantGroup
|
||||
from users.filterset_mixins import OwnerFilterMixin
|
||||
from users.models import Group, User
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
@@ -49,7 +49,6 @@ class ScriptFilterSet(BaseFilterSet):
|
||||
)
|
||||
module_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ScriptModule.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Script module (ID)'),
|
||||
)
|
||||
|
||||
@@ -72,8 +71,7 @@ class WebhookFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
label=_('Search'),
|
||||
)
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices,
|
||||
distinct=False,
|
||||
choices=WebhookHttpMethodChoices
|
||||
)
|
||||
payload_url = MultiValueCharFilter(
|
||||
lookup_expr='icontains'
|
||||
@@ -106,17 +104,16 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
event_type = MultiValueCharFilter(
|
||||
method='filter_event_type'
|
||||
)
|
||||
action_type = django_filters.MultipleChoiceFilter(
|
||||
choices=EventRuleActionChoices,
|
||||
distinct=False,
|
||||
choices=EventRuleActionChoices
|
||||
)
|
||||
action_object_type = MultiValueContentTypeFilter()
|
||||
action_object_type = ContentTypeFilter()
|
||||
action_object_id = MultiValueNumberFilter()
|
||||
|
||||
class Meta:
|
||||
@@ -145,30 +142,26 @@ class CustomFieldFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
label=_('Search'),
|
||||
)
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CustomFieldTypeChoices,
|
||||
distinct=False,
|
||||
choices=CustomFieldTypeChoices
|
||||
)
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
related_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
distinct=False,
|
||||
field_name='related_object_type'
|
||||
)
|
||||
related_object_type = MultiValueContentTypeFilter()
|
||||
related_object_type = ContentTypeFilter()
|
||||
choice_set_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
distinct=False,
|
||||
queryset=CustomFieldChoiceSet.objects.all()
|
||||
)
|
||||
choice_set = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='choice_set__name',
|
||||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
@@ -231,7 +224,7 @@ class CustomLinkFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
|
||||
@@ -262,17 +255,15 @@ class ExportTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data source (ID)'),
|
||||
)
|
||||
data_file_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data file (ID)'),
|
||||
)
|
||||
|
||||
@@ -303,18 +294,16 @@ class SavedFilterFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User (name)'),
|
||||
)
|
||||
@@ -356,21 +345,18 @@ class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
distinct=False,
|
||||
field_name='object_type'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_type'
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User (name)'),
|
||||
)
|
||||
@@ -409,16 +395,14 @@ class TableConfigFilterSet(ChangeLoggedModelFilterSet):
|
||||
class BookmarkFilterSet(BaseFilterSet):
|
||||
created = django_filters.DateTimeFilter()
|
||||
object_type_id = MultiValueNumberFilter()
|
||||
object_type = MultiValueContentTypeFilter()
|
||||
object_type = ContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User (name)'),
|
||||
)
|
||||
@@ -478,7 +462,7 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter()
|
||||
object_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
@@ -497,26 +481,22 @@ class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
@register_filterset
|
||||
class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = MultiValueContentTypeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
assigned_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all(),
|
||||
distinct=False,
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='created_by__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User (name)'),
|
||||
)
|
||||
kind = django_filters.MultipleChoiceFilter(
|
||||
choices=JournalEntryKindChoices,
|
||||
distinct=False,
|
||||
choices=JournalEntryKindChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -596,22 +576,19 @@ class TaggedItemFilterSet(BaseFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='content_type'
|
||||
)
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all(),
|
||||
distinct=False,
|
||||
field_name='content_type_id'
|
||||
)
|
||||
tag_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tag.objects.all(),
|
||||
distinct=False,
|
||||
queryset=Tag.objects.all()
|
||||
)
|
||||
tag = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tag__slug',
|
||||
queryset=Tag.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
)
|
||||
|
||||
@@ -637,12 +614,10 @@ class ConfigContextProfileFilterSet(PrimaryModelFilterSet):
|
||||
)
|
||||
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data source (ID)'),
|
||||
)
|
||||
data_file_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data file (ID)'),
|
||||
)
|
||||
|
||||
@@ -670,13 +645,11 @@ class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
profile_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigContextProfile.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Profile (ID)'),
|
||||
)
|
||||
profile = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='profile__name',
|
||||
queryset=ConfigContextProfile.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Profile (name)'),
|
||||
)
|
||||
@@ -813,12 +786,10 @@ class ConfigContextFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data source (ID)'),
|
||||
)
|
||||
data_file_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data file (ID)'),
|
||||
)
|
||||
|
||||
@@ -844,12 +815,10 @@ class ConfigTemplateFilterSet(OwnerFilterMixin, ChangeLoggedModelFilterSet):
|
||||
)
|
||||
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data source (ID)'),
|
||||
)
|
||||
data_file_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Data file (ID)'),
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
@@ -178,11 +178,9 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
||||
name=name,
|
||||
is_executable=True,
|
||||
)
|
||||
sync_classes.alters_data = True
|
||||
|
||||
def sync_data(self):
|
||||
super().sync_data()
|
||||
sync_data.alters_data = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.file_root = ManagedFileRootPathChoices.SCRIPTS
|
||||
|
||||
@@ -304,7 +304,7 @@ class ConditionSetTest(TestCase):
|
||||
Test Event Rule with incorrect condition (key "foo" is wrong). Must return false.
|
||||
"""
|
||||
|
||||
ct = ContentType.objects.get_by_natural_key('extras', 'webhook')
|
||||
ct = ContentType.objects.get(app_label='extras', model='webhook')
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST')
|
||||
form = EventRuleForm({
|
||||
|
||||
@@ -111,13 +111,13 @@ class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_related_object_type(self):
|
||||
params = {'related_object_type': ['dcim.site']}
|
||||
params = {'related_object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'related_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -348,7 +348,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.region']}
|
||||
params = {'object_type': 'dcim.region'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'object_type_id': [ObjectType.objects.get_for_model(Region).pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -417,7 +417,7 @@ class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -508,7 +508,7 @@ class SavedFilterTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -600,7 +600,7 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
|
||||
Bookmark.objects.bulk_create(bookmarks)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'object_type_id': [ContentType.objects.get_for_model(Site).pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@@ -663,7 +663,7 @@ class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -697,8 +697,8 @@ class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site')
|
||||
rack_ct = ContentType.objects.get_by_natural_key('dcim', 'rack')
|
||||
site_ct = ContentType.objects.get(app_label='dcim', model='site')
|
||||
rack_ct = ContentType.objects.get(app_label='dcim', model='rack')
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
@@ -757,12 +757,12 @@ class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_object_type_id_and_object_id(self):
|
||||
params = {
|
||||
'object_type_id': ContentType.objects.get_by_natural_key('dcim', 'site').pk,
|
||||
'object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk,
|
||||
'object_id': [Site.objects.first().pk],
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -845,14 +845,14 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_assigned_object_type(self):
|
||||
params = {'assigned_object_type': ['dcim.site']}
|
||||
params = {'assigned_object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'assigned_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]}
|
||||
params = {'assigned_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_assigned_object(self):
|
||||
params = {
|
||||
'assigned_object_type': ['dcim.site'],
|
||||
'assigned_object_type': 'dcim.site',
|
||||
'assigned_object_id': [Site.objects.first().pk],
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1426,15 +1426,15 @@ class TaggedItemFilterSetTestCase(TestCase):
|
||||
|
||||
def test_object_type(self):
|
||||
object_type = ObjectType.objects.get_for_model(Site)
|
||||
params = {'object_type': ['dcim.site']}
|
||||
params = {'object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'object_type_id': [object_type.pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_object(self):
|
||||
def test_object_id(self):
|
||||
site_ids = Site.objects.values_list('pk', flat=True)
|
||||
params = {
|
||||
'object_type': ['dcim.site'],
|
||||
'object_type': 'dcim.site',
|
||||
'object_id': site_ids[:2],
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -17,7 +17,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac
|
||||
class ImageAttachmentTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack')
|
||||
cls.ct_rack = ContentType.objects.get(app_label='dcim', model='rack')
|
||||
cls.image_content = b''
|
||||
|
||||
def _stub_image_attachment(self, object_id, image_filename, name=None):
|
||||
|
||||
@@ -27,7 +27,7 @@ class ImageUploadTests(TestCase):
|
||||
def setUpTestData(cls):
|
||||
# We only need a ContentType with model="rack" for the prefix;
|
||||
# this doesn't require creating a Rack object.
|
||||
cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack')
|
||||
cls.ct_rack = ContentType.objects.get(app_label='dcim', model='rack')
|
||||
|
||||
def _stub_instance(self, object_id=12, name=None):
|
||||
"""
|
||||
|
||||
@@ -16,8 +16,7 @@ from netbox.filtersets import (
|
||||
)
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter, NumericArrayFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
@@ -167,13 +166,11 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
)
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RIR.objects.all(),
|
||||
distinct=False,
|
||||
label=_('RIR (ID)'),
|
||||
)
|
||||
rir = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rir__slug',
|
||||
queryset=RIR.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('RIR (slug)'),
|
||||
)
|
||||
@@ -209,13 +206,11 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RIR.objects.all(),
|
||||
distinct=False,
|
||||
label=_('RIR (ID)'),
|
||||
)
|
||||
rir = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rir__slug',
|
||||
queryset=RIR.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('RIR (slug)'),
|
||||
)
|
||||
@@ -237,13 +232,11 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RIR.objects.all(),
|
||||
distinct=False,
|
||||
label=_('RIR (ID)'),
|
||||
)
|
||||
rir = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rir__slug',
|
||||
queryset=RIR.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('RIR (slug)'),
|
||||
)
|
||||
@@ -349,13 +342,11 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
distinct=False,
|
||||
label=_('VRF'),
|
||||
)
|
||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vrf__rd',
|
||||
queryset=VRF.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='rd',
|
||||
label=_('VRF (RD)'),
|
||||
)
|
||||
@@ -373,20 +364,17 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
||||
vlan_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan__group',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='id',
|
||||
label=_('VLAN Group (ID)'),
|
||||
)
|
||||
vlan_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vlan__group__slug',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('VLAN Group (slug)'),
|
||||
)
|
||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLAN.objects.all(),
|
||||
distinct=False,
|
||||
label=_('VLAN (ID)'),
|
||||
)
|
||||
vlan_vid = django_filters.NumberFilter(
|
||||
@@ -395,19 +383,16 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Role (ID)'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Role (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=PrefixStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -501,31 +486,26 @@ class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
distinct=False,
|
||||
label=_('VRF'),
|
||||
)
|
||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vrf__rd',
|
||||
queryset=VRF.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='rd',
|
||||
label=_('VRF (RD)'),
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Role (ID)'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Role (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=IPRangeStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
parent = MultiValueCharFilter(
|
||||
@@ -608,13 +588,11 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
distinct=False,
|
||||
label=_('VRF'),
|
||||
)
|
||||
vrf = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vrf__rd',
|
||||
queryset=VRF.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='rd',
|
||||
label=_('VRF (RD)'),
|
||||
)
|
||||
@@ -629,7 +607,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
to_field_name='rd',
|
||||
label=_('VRF (RD)'),
|
||||
)
|
||||
assigned_object_type = MultiValueContentTypeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
@@ -687,12 +665,10 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=IPAddressStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=IPAddressRoleChoices,
|
||||
distinct=False,
|
||||
choices=IPAddressRoleChoices
|
||||
)
|
||||
service_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='services',
|
||||
@@ -702,7 +678,6 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
nat_inside_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='nat_inside',
|
||||
queryset=IPAddress.objects.all(),
|
||||
distinct=False,
|
||||
label=_('NAT inside IP address (ID)'),
|
||||
)
|
||||
|
||||
@@ -824,12 +799,10 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
@register_filterset
|
||||
class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
||||
protocol = django_filters.MultipleChoiceFilter(
|
||||
choices=FHRPGroupProtocolChoices,
|
||||
distinct=False,
|
||||
choices=FHRPGroupProtocolChoices
|
||||
)
|
||||
auth_type = django_filters.MultipleChoiceFilter(
|
||||
choices=FHRPGroupAuthTypeChoices,
|
||||
distinct=False,
|
||||
choices=FHRPGroupAuthTypeChoices
|
||||
)
|
||||
related_ip = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=IPAddress.objects.all(),
|
||||
@@ -873,10 +846,9 @@ class FHRPGroupFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
@register_filterset
|
||||
class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
interface_type = MultiValueContentTypeFilter()
|
||||
interface_type = ContentTypeFilter()
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=FHRPGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Group (ID)'),
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
@@ -929,7 +901,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
@register_filterset
|
||||
class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
scope_type = MultiValueContentTypeFilter()
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
)
|
||||
@@ -1007,43 +979,36 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Group'),
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Role (ID)'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Role (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VLANStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
available_at_site = django_filters.ModelChoiceFilter(
|
||||
@@ -1059,12 +1024,10 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
method='get_for_virtualmachine'
|
||||
)
|
||||
qinq_role = django_filters.MultipleChoiceFilter(
|
||||
choices=VLANQinQRoleChoices,
|
||||
distinct=False,
|
||||
choices=VLANQinQRoleChoices
|
||||
)
|
||||
qinq_svlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLAN.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Q-in-Q SVLAN (ID)'),
|
||||
)
|
||||
qinq_svlan_vid = MultiValueNumberFilter(
|
||||
@@ -1159,13 +1122,11 @@ class VLANTranslationPolicyFilterSet(PrimaryModelFilterSet):
|
||||
class VLANTranslationRuleFilterSet(NetBoxModelFilterSet):
|
||||
policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
distinct=False,
|
||||
label=_('VLAN Translation Policy (ID)'),
|
||||
)
|
||||
policy = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='policy__name',
|
||||
queryset=VLANTranslationPolicy.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('VLAN Translation Policy (name)'),
|
||||
)
|
||||
@@ -1212,7 +1173,7 @@ class ServiceTemplateFilterSet(PrimaryModelFilterSet):
|
||||
|
||||
@register_filterset
|
||||
class ServiceFilterSet(ContactModelFilterSet, PrimaryModelFilterSet):
|
||||
parent_object_type = MultiValueContentTypeFilter()
|
||||
parent_object_type = ContentTypeFilter()
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
@@ -1304,26 +1265,22 @@ class PrimaryIPFilterSet(django_filters.FilterSet):
|
||||
primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_ip4',
|
||||
queryset=IPAddress.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Primary IPv4 (ID)'),
|
||||
)
|
||||
primary_ip4 = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_ip4__address',
|
||||
queryset=IPAddress.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='address',
|
||||
label=_('Primary IPv4 (address)'),
|
||||
)
|
||||
primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_ip6',
|
||||
queryset=IPAddress.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Primary IPv6 (ID)'),
|
||||
)
|
||||
primary_ip6 = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_ip6__address',
|
||||
queryset=IPAddress.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='address',
|
||||
label=_('Primary IPv6 (address)'),
|
||||
)
|
||||
|
||||
@@ -1572,12 +1572,12 @@ class FHRPGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_interface_type(self):
|
||||
params = {'interface_type': ['dcim.interface']}
|
||||
params = {'interface_type': 'dcim.interface'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_interface(self):
|
||||
interfaces = Interface.objects.all()[:2]
|
||||
params = {'interface_type': ['dcim.interface'], 'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
params = {'interface_type': 'dcim.interface', 'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_priority(self):
|
||||
|
||||
@@ -143,10 +143,6 @@ class NestedGroupModel(OwnerMixin, NetBoxModel, MPTTModel):
|
||||
"""
|
||||
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
||||
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
||||
|
||||
Note: django-mptt injects the (tree_id, lft) index dynamically, but Django's migration autodetector won't
|
||||
detect it unless concrete subclasses explicitly declare Meta.indexes (even as an empty tuple). See #21016
|
||||
and django-mptt/django-mptt#682.
|
||||
"""
|
||||
parent = TreeForeignKey(
|
||||
to='self',
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
|
||||
from netbox.filtersets import (
|
||||
NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||
)
|
||||
from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from .models import *
|
||||
|
||||
@@ -29,13 +29,11 @@ __all__ = (
|
||||
class ContactGroupFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContactGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Parent contact group (ID)'),
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=ContactGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Parent contact group (slug)'),
|
||||
)
|
||||
@@ -112,10 +110,9 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter()
|
||||
object_type = ContentTypeFilter()
|
||||
contact_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Contact.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Contact (ID)'),
|
||||
)
|
||||
group_id = TreeNodeMultipleChoiceFilter(
|
||||
@@ -133,13 +130,11 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContactRole.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Contact role (ID)'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=ContactRole.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Contact role (slug)'),
|
||||
)
|
||||
@@ -184,13 +179,11 @@ class ContactModelFilterSet(django_filters.FilterSet):
|
||||
class TenantGroupFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Parent tenant group (ID)'),
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Parent tenant group (slug)'),
|
||||
)
|
||||
@@ -263,12 +256,10 @@ class TenancyFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Tenant (ID)'),
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
distinct=False,
|
||||
field_name='tenant__slug',
|
||||
to_field_name='slug',
|
||||
label=_('Tenant (slug)'),
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-13 13:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0134_owner'),
|
||||
('tenancy', '0022_add_comments_to_organizationalmodel'),
|
||||
('users', '0015_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='contactgroup',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='tenancy_contactgroup_tree_d2ce'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tenantgroup',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='tenancy_tenantgroup_tree_ifebc'),
|
||||
),
|
||||
]
|
||||
@@ -22,9 +22,6 @@ class ContactGroup(NestedGroupModel):
|
||||
"""
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
|
||||
@@ -29,9 +29,6 @@ class TenantGroup(NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
verbose_name = _('tenant group')
|
||||
verbose_name_plural = _('tenant groups')
|
||||
|
||||
|
||||
@@ -355,8 +355,6 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ContactAssignment.objects.bulk_create(assignments)
|
||||
|
||||
def test_object_type(self):
|
||||
params = {'object_type': ['dcim.site']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'object_type_id': ObjectType.objects.get_by_natural_key('dcim', 'site')}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-13 05:26+0000\n"
|
||||
"POT-Creation-Date: 2026-02-12 05:28+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -12716,67 +12716,67 @@ msgstr ""
|
||||
msgid "Cannot delete stores from registry"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:847
|
||||
#: netbox/netbox/settings.py:837
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:848
|
||||
#: netbox/netbox/settings.py:838
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:849
|
||||
#: netbox/netbox/settings.py:839
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:850
|
||||
#: netbox/netbox/settings.py:840
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:851
|
||||
#: netbox/netbox/settings.py:841
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:852
|
||||
#: netbox/netbox/settings.py:842
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:853
|
||||
#: netbox/netbox/settings.py:843
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:854
|
||||
#: netbox/netbox/settings.py:844
|
||||
msgid "Japanese"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:855
|
||||
#: netbox/netbox/settings.py:845
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:856
|
||||
#: netbox/netbox/settings.py:846
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:857
|
||||
#: netbox/netbox/settings.py:847
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:858
|
||||
#: netbox/netbox/settings.py:848
|
||||
msgid "Portuguese"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:859
|
||||
#: netbox/netbox/settings.py:849
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:860
|
||||
#: netbox/netbox/settings.py:850
|
||||
msgid "Turkish"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:861
|
||||
#: netbox/netbox/settings.py:851
|
||||
msgid "Ukrainian"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/settings.py:862
|
||||
#: netbox/netbox/settings.py:852
|
||||
msgid "Chinese"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -14,26 +14,22 @@ class OwnerFilterMixin(django_filters.FilterSet):
|
||||
"""
|
||||
owner_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=OwnerGroup.objects.all(),
|
||||
distinct=False,
|
||||
field_name='owner__group',
|
||||
label=_('Owner Group (ID)'),
|
||||
)
|
||||
owner_group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=OwnerGroup.objects.all(),
|
||||
distinct=False,
|
||||
field_name='owner__group__name',
|
||||
to_field_name='name',
|
||||
label=_('Owner Group (name)'),
|
||||
)
|
||||
owner_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Owner.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Owner (ID)'),
|
||||
)
|
||||
owner = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='owner__name',
|
||||
queryset=Owner.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Owner (name)'),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from core.models import ObjectType
|
||||
from extras.models import NotificationGroup
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from users.models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
||||
from utilities.filters import MultiValueContentTypeFilter
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
|
||||
__all__ = (
|
||||
@@ -131,13 +131,11 @@ class TokenFilterSet(BaseFilterSet):
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
label=_('User'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='username',
|
||||
label=_('User (name)'),
|
||||
)
|
||||
@@ -196,7 +194,7 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = MultiValueContentTypeFilter(
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
can_view = django_filters.BooleanFilter(
|
||||
@@ -282,13 +280,11 @@ class OwnerFilterSet(BaseFilterSet):
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=OwnerGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__name',
|
||||
queryset=OwnerGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Group (name)'),
|
||||
)
|
||||
|
||||
@@ -113,7 +113,6 @@ class UserConfig(models.Model):
|
||||
|
||||
if commit:
|
||||
self.save()
|
||||
set.alters_data = True
|
||||
|
||||
def clear(self, path, commit=False):
|
||||
"""
|
||||
@@ -141,4 +140,3 @@ class UserConfig(models.Model):
|
||||
|
||||
if commit:
|
||||
self.save()
|
||||
clear.alters_data = True
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import django_filters
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_filters.constants import EMPTY_VALUES
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -11,7 +10,6 @@ __all__ = (
|
||||
'ContentTypeFilter',
|
||||
'MultiValueArrayFilter',
|
||||
'MultiValueCharFilter',
|
||||
'MultiValueContentTypeFilter',
|
||||
'MultiValueDateFilter',
|
||||
'MultiValueDateTimeFilter',
|
||||
'MultiValueDecimalFilter',
|
||||
@@ -173,27 +171,3 @@ class ContentTypeFilter(django_filters.CharFilter):
|
||||
f'{self.field_name}__model': model
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MultiValueContentTypeFilter(MultiValueCharFilter):
|
||||
"""
|
||||
A multi-value version of ContentTypeFilter.
|
||||
"""
|
||||
def filter(self, qs, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
|
||||
content_types = []
|
||||
for key in value:
|
||||
try:
|
||||
app_label, model = key.lower().split('.')
|
||||
ct = ContentType.objects.get_by_natural_key(app_label, model)
|
||||
content_types.append(ct)
|
||||
except (ValueError, ContentType.DoesNotExist):
|
||||
continue
|
||||
|
||||
return qs.filter(
|
||||
**{
|
||||
f'{self.field_name}__in': content_types,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from mptt.models import MPTTModel
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from extras.filters import TagFilter
|
||||
from utilities.filters import MultiValueContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
|
||||
__all__ = (
|
||||
'BaseFilterSetTests',
|
||||
@@ -75,7 +75,7 @@ class BaseFilterSetTests:
|
||||
# Standardize on object_type for filter name even though it's technically a ContentType
|
||||
filter_name = 'object_type'
|
||||
return [
|
||||
(filter_name, MultiValueContentTypeFilter),
|
||||
(filter_name, ContentTypeFilter),
|
||||
(f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import django_filters
|
||||
import netaddr
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.base_filtersets import ScopedFilterSet
|
||||
from dcim.filtersets import CommonInterfaceFilterSet
|
||||
@@ -49,31 +47,26 @@ class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
||||
class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ScopedFilterSet, ContactModelFilterSet):
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Parent group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Parent group (slug)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ClusterType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Cluster type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=ClusterType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Cluster type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=ClusterStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
@@ -101,61 +94,51 @@ class VirtualMachineFilterSet(
|
||||
):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualMachineStatusChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
start_on_boot = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualMachineStartOnBootChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__group',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Cluster group (ID)'),
|
||||
)
|
||||
cluster_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__group__slug',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Cluster group (slug)'),
|
||||
)
|
||||
cluster_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__type',
|
||||
queryset=ClusterType.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Cluster type (ID)'),
|
||||
)
|
||||
cluster_type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__type__slug',
|
||||
queryset=ClusterType.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Cluster type (slug)'),
|
||||
)
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Cluster.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Cluster (ID)'),
|
||||
)
|
||||
cluster = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster__name',
|
||||
queryset=Cluster.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Cluster'),
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Device.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Device (ID)'),
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device__name',
|
||||
queryset=Device.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Device'),
|
||||
)
|
||||
@@ -187,13 +170,11 @@ class VirtualMachineFilterSet(
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
@@ -235,7 +216,6 @@ class VirtualMachineFilterSet(
|
||||
)
|
||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Config template (ID)'),
|
||||
)
|
||||
|
||||
@@ -249,22 +229,14 @@ class VirtualMachineFilterSet(
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value) |
|
||||
Q(primary_ip4__address__startswith=value) |
|
||||
Q(primary_ip6__address__startswith=value) |
|
||||
Q(serial__icontains=value)
|
||||
)
|
||||
# If the given value looks like an IP address, look for primary IPv4/IPv6 assignments
|
||||
try:
|
||||
ipaddress = netaddr.IPNetwork(value)
|
||||
if ipaddress.version == 4:
|
||||
qs_filter |= Q(primary_ip4__address__host__inet=ipaddress.ip)
|
||||
elif ipaddress.version == 6:
|
||||
qs_filter |= Q(primary_ip6__address__host__inet=ipaddress.ip)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||
@@ -278,39 +250,33 @@ class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxMod
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine__cluster',
|
||||
queryset=Cluster.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Cluster (ID)'),
|
||||
)
|
||||
cluster = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine__cluster__name',
|
||||
queryset=Cluster.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Cluster'),
|
||||
)
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual machine (ID)'),
|
||||
)
|
||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine__name',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Virtual machine'),
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent',
|
||||
queryset=VMInterface.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Parent interface (ID)'),
|
||||
)
|
||||
bridge_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='bridge',
|
||||
queryset=VMInterface.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Bridged interface (ID)'),
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter(
|
||||
@@ -320,13 +286,11 @@ class VMInterfaceFilterSet(CommonInterfaceFilterSet, OwnerFilterMixin, NetBoxMod
|
||||
primary_mac_address_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_mac_address',
|
||||
queryset=MACAddress.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Primary MAC address (ID)'),
|
||||
)
|
||||
primary_mac_address = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='primary_mac_address__mac_address',
|
||||
queryset=MACAddress.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='mac_address',
|
||||
label=_('Primary MAC address'),
|
||||
)
|
||||
@@ -349,13 +313,11 @@ class VirtualDiskFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
|
||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Virtual machine (ID)'),
|
||||
)
|
||||
virtual_machine = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_machine__name',
|
||||
queryset=VirtualMachine.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Virtual machine'),
|
||||
)
|
||||
|
||||
@@ -320,7 +320,6 @@ class ClusterAddDevicesView(generic.ObjectEditView):
|
||||
|
||||
# Assign the selected Devices to the Cluster
|
||||
for device in Device.objects.filter(pk__in=device_pks):
|
||||
device.snapshot()
|
||||
device.cluster = cluster
|
||||
device.save()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from dcim.models import Device, Interface
|
||||
from ipam.models import IPAddress, RouteTarget, VLAN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter
|
||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
from utilities.filtersets import register_filterset
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
@@ -38,34 +38,28 @@ class TunnelGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
@register_filterset
|
||||
class TunnelFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=TunnelStatusChoices,
|
||||
distinct=False,
|
||||
choices=TunnelStatusChoices
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=TunnelGroup.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Tunnel group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=TunnelGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Tunnel group (slug)'),
|
||||
)
|
||||
encapsulation = django_filters.MultipleChoiceFilter(
|
||||
choices=TunnelEncapsulationChoices,
|
||||
distinct=False,
|
||||
choices=TunnelEncapsulationChoices
|
||||
)
|
||||
ipsec_profile_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=IPSecProfile.objects.all(),
|
||||
distinct=False,
|
||||
label=_('IPSec profile (ID)'),
|
||||
)
|
||||
ipsec_profile = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ipsec_profile__name',
|
||||
queryset=IPSecProfile.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('IPSec profile (name)'),
|
||||
)
|
||||
@@ -89,21 +83,18 @@ class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
||||
tunnel_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tunnel',
|
||||
queryset=Tunnel.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Tunnel (ID)'),
|
||||
)
|
||||
tunnel = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tunnel__name',
|
||||
queryset=Tunnel.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('Tunnel (name)'),
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=TunnelTerminationRoleChoices,
|
||||
distinct=False,
|
||||
choices=TunnelTerminationRoleChoices
|
||||
)
|
||||
termination_type = MultiValueContentTypeFilter()
|
||||
termination_type = ContentTypeFilter()
|
||||
interface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__name',
|
||||
queryset=Interface.objects.all(),
|
||||
@@ -129,7 +120,6 @@ class TunnelTerminationFilterSet(NetBoxModelFilterSet):
|
||||
outside_ip_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='outside_ip',
|
||||
queryset=IPAddress.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Outside IP (ID)'),
|
||||
)
|
||||
|
||||
@@ -152,20 +142,16 @@ class IKEProposalFilterSet(PrimaryModelFilterSet):
|
||||
label=_('IKE policy (name)'),
|
||||
)
|
||||
authentication_method = django_filters.MultipleChoiceFilter(
|
||||
choices=AuthenticationMethodChoices,
|
||||
distinct=False,
|
||||
choices=AuthenticationMethodChoices
|
||||
)
|
||||
encryption_algorithm = django_filters.MultipleChoiceFilter(
|
||||
choices=EncryptionAlgorithmChoices,
|
||||
distinct=False,
|
||||
choices=EncryptionAlgorithmChoices
|
||||
)
|
||||
authentication_algorithm = django_filters.MultipleChoiceFilter(
|
||||
choices=AuthenticationAlgorithmChoices,
|
||||
distinct=False,
|
||||
choices=AuthenticationAlgorithmChoices
|
||||
)
|
||||
group = django_filters.MultipleChoiceFilter(
|
||||
choices=DHGroupChoices,
|
||||
distinct=False,
|
||||
choices=DHGroupChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -185,12 +171,10 @@ class IKEProposalFilterSet(PrimaryModelFilterSet):
|
||||
@register_filterset
|
||||
class IKEPolicyFilterSet(PrimaryModelFilterSet):
|
||||
version = django_filters.MultipleChoiceFilter(
|
||||
choices=IKEVersionChoices,
|
||||
distinct=False,
|
||||
choices=IKEVersionChoices
|
||||
)
|
||||
mode = django_filters.MultipleChoiceFilter(
|
||||
choices=IKEModeChoices,
|
||||
distinct=False,
|
||||
choices=IKEModeChoices
|
||||
)
|
||||
ike_proposal_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='proposals',
|
||||
@@ -230,12 +214,10 @@ class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
||||
label=_('IPSec policy (name)'),
|
||||
)
|
||||
encryption_algorithm = django_filters.MultipleChoiceFilter(
|
||||
choices=EncryptionAlgorithmChoices,
|
||||
distinct=False,
|
||||
choices=EncryptionAlgorithmChoices
|
||||
)
|
||||
authentication_algorithm = django_filters.MultipleChoiceFilter(
|
||||
choices=AuthenticationAlgorithmChoices,
|
||||
distinct=False,
|
||||
choices=AuthenticationAlgorithmChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -255,8 +237,7 @@ class IPSecProposalFilterSet(PrimaryModelFilterSet):
|
||||
@register_filterset
|
||||
class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
||||
pfs_group = django_filters.MultipleChoiceFilter(
|
||||
choices=DHGroupChoices,
|
||||
distinct=False,
|
||||
choices=DHGroupChoices
|
||||
)
|
||||
ipsec_proposal_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='proposals',
|
||||
@@ -285,30 +266,25 @@ class IPSecPolicyFilterSet(PrimaryModelFilterSet):
|
||||
@register_filterset
|
||||
class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
||||
mode = django_filters.MultipleChoiceFilter(
|
||||
choices=IPSecModeChoices,
|
||||
distinct=False,
|
||||
choices=IPSecModeChoices
|
||||
)
|
||||
ike_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=IKEPolicy.objects.all(),
|
||||
distinct=False,
|
||||
label=_('IKE policy (ID)'),
|
||||
)
|
||||
ike_policy = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ike_policy__name',
|
||||
queryset=IKEPolicy.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('IKE policy (name)'),
|
||||
)
|
||||
ipsec_policy_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=IPSecPolicy.objects.all(),
|
||||
distinct=False,
|
||||
label=_('IPSec policy (ID)'),
|
||||
)
|
||||
ipsec_policy = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='ipsec_policy__name',
|
||||
queryset=IPSecPolicy.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='name',
|
||||
label=_('IPSec policy (name)'),
|
||||
)
|
||||
@@ -331,12 +307,10 @@ class IPSecProfileFilterSet(PrimaryModelFilterSet):
|
||||
class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=L2VPNTypeChoices,
|
||||
distinct=False,
|
||||
null_value=None
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=L2VPNStatusChoices,
|
||||
distinct=False,
|
||||
)
|
||||
import_target_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='import_targets',
|
||||
@@ -380,13 +354,11 @@ class L2VPNFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilter
|
||||
class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
l2vpn_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=L2VPN.objects.all(),
|
||||
distinct=False,
|
||||
label=_('L2VPN (ID)'),
|
||||
)
|
||||
l2vpn = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='l2vpn__slug',
|
||||
queryset=L2VPN.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('L2VPN (slug)'),
|
||||
)
|
||||
@@ -471,10 +443,9 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
assigned_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
distinct=False,
|
||||
field_name='assigned_object_type'
|
||||
)
|
||||
assigned_object_type = MultiValueContentTypeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = L2VPNTermination
|
||||
|
||||
@@ -268,9 +268,9 @@ class TunnelTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_termination_type(self):
|
||||
params = {'termination_type': ['dcim.interface']}
|
||||
params = {'termination_type': 'dcim.interface'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'termination_type': ['virtualization.vminterface']}
|
||||
params = {'termination_type': 'virtualization.vminterface'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_interface(self):
|
||||
@@ -902,7 +902,7 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_termination_type(self):
|
||||
params = {'assigned_object_type': ['ipam.vlan']}
|
||||
params = {'assigned_object_type': 'ipam.vlan'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_interface(self):
|
||||
|
||||
@@ -22,13 +22,11 @@ __all__ = (
|
||||
@register_filterset
|
||||
class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
distinct=False,
|
||||
queryset=WirelessLANGroup.objects.all()
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__slug',
|
||||
queryset=WirelessLANGroup.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug'
|
||||
)
|
||||
ancestor_id = TreeNodeMultipleChoiceFilter(
|
||||
@@ -62,24 +60,20 @@ class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilter
|
||||
to_field_name='slug'
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessLANStatusChoices,
|
||||
distinct=False,
|
||||
choices=WirelessLANStatusChoices
|
||||
)
|
||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VLAN.objects.all(),
|
||||
distinct=False,
|
||||
queryset=VLAN.objects.all()
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
field_name='interfaces'
|
||||
)
|
||||
auth_type = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessAuthTypeChoices,
|
||||
distinct=False,
|
||||
choices=WirelessAuthTypeChoices
|
||||
)
|
||||
auth_cipher = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessAuthCipherChoices,
|
||||
distinct=False,
|
||||
choices=WirelessAuthCipherChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -99,24 +93,19 @@ class WirelessLANFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilter
|
||||
@register_filterset
|
||||
class WirelessLinkFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
interface_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
distinct=False,
|
||||
queryset=Interface.objects.all()
|
||||
)
|
||||
interface_b_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
distinct=False,
|
||||
queryset=Interface.objects.all()
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=LinkStatusChoices,
|
||||
distinct=False,
|
||||
choices=LinkStatusChoices
|
||||
)
|
||||
auth_type = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessAuthTypeChoices,
|
||||
distinct=False,
|
||||
choices=WirelessAuthTypeChoices
|
||||
)
|
||||
auth_cipher = django_filters.MultipleChoiceFilter(
|
||||
choices=WirelessAuthCipherChoices,
|
||||
distinct=False,
|
||||
choices=WirelessAuthCipherChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.10 on 2026-02-13 13:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0134_owner'),
|
||||
('users', '0015_owner'),
|
||||
('wireless', '0017_gfk_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='wirelesslangroup',
|
||||
index=models.Index(fields=['tree_id', 'lft'], name='wireless_wirelesslangroup_fbcd'),
|
||||
),
|
||||
]
|
||||
@@ -63,9 +63,6 @@ class WirelessLANGroup(NestedGroupModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk')
|
||||
# Empty tuple triggers Django migration detection for MPTT indexes
|
||||
# (see #21016, django-mptt/django-mptt#682)
|
||||
indexes = ()
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('parent', 'name'),
|
||||
|
||||
@@ -305,7 +305,7 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_scope_type(self):
|
||||
params = {'scope_type': ['dcim.location']}
|
||||
params = {'scope_type': 'dcim.location'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ django-debug-toolbar==6.2.0
|
||||
django-filter==25.2
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
django-htmx==1.27.0
|
||||
django-mptt==0.18.0
|
||||
django-mptt==0.17.0
|
||||
django-pglocks==1.0.4
|
||||
django-prometheus==2.4.1
|
||||
django-redis==6.0.0
|
||||
|
||||
Reference in New Issue
Block a user