mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Closes #4897: Allow filtering by content type identified as <app>.<model> string
This commit is contained in:
parent
5ba4252388
commit
0c3fafdfef
@ -35,6 +35,7 @@ http://netbox/api/dcim/sites/ \
|
|||||||
* [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines
|
* [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines
|
||||||
* [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI
|
* [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI
|
||||||
* [#2179](https://github.com/netbox-community/netbox/issues/2179) - Support the assignment of multiple port numbers for services
|
* [#2179](https://github.com/netbox-community/netbox/issues/2179) - Support the assignment of multiple port numbers for services
|
||||||
|
* [#4897](https://github.com/netbox-community/netbox/issues/4897) - Allow filtering by content type identified as `<app>.<model>` string
|
||||||
* [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view
|
* [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view
|
||||||
* [#5003](https://github.com/netbox-community/netbox/issues/5003) - CSV import now accepts slug values for choice fields
|
* [#5003](https://github.com/netbox-community/netbox/issues/5003) - CSV import now accepts slug values for choice fields
|
||||||
* [#5146](https://github.com/netbox-community/netbox/issues/5146) - Add custom fields support for cables, power panels, rack reservations, and virtual chassis
|
* [#5146](https://github.com/netbox-community/netbox/issues/5146) - Add custom fields support for cables, power panels, rack reservations, and virtual chassis
|
||||||
@ -57,5 +58,7 @@ http://netbox/api/dcim/sites/ \
|
|||||||
* dcim.VirtualChassis: Added `custom_fields`
|
* dcim.VirtualChassis: Added `custom_fields`
|
||||||
* extras.ExportTemplate: The `template_language` field has been removed
|
* extras.ExportTemplate: The `template_language` field has been removed
|
||||||
* extras.Graph: This API endpoint has been removed (see #4349)
|
* extras.Graph: This API endpoint has been removed (see #4349)
|
||||||
|
* extras.ImageAttachment: Filtering by `content_type` now takes a string in the form `<app>.<model>`
|
||||||
|
* extras.ObjectChange: Filtering by `changed_object_type` now takes a string in the form `<app>.<model>`
|
||||||
* ipam.Service: Renamed `port` to `ports`; now holds a list of one or more port numbers
|
* ipam.Service: Renamed `port` to `ports`; now holds a list of one or more port numbers
|
||||||
* secrets.Secret: Removed `device` field; replaced with `assigned_object` generic foreign key. This may represent either a device or a virtual machine. Assign an object by setting `assigned_object_type` and `assigned_object_id`.
|
* secrets.Secret: Removed `device` field; replaced with `assigned_object` generic foreign key. This may represent either a device or a virtual machine. Assign an object by setting `assigned_object_type` and `assigned_object_id`.
|
||||||
|
@ -4,7 +4,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.filters import BaseFilterSet
|
from utilities.filters import BaseFilterSet, ContentTypeFilter
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, JobResult, ObjectChange, Tag
|
from .models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, JobResult, ObjectChange, Tag
|
||||||
@ -81,10 +81,11 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||||
|
content_type = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
fields = ['id', 'content_type', 'object_id', 'name']
|
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||||
|
|
||||||
|
|
||||||
class TagFilterSet(BaseFilterSet):
|
class TagFilterSet(BaseFilterSet):
|
||||||
@ -234,11 +235,12 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
time = django_filters.DateTimeFromToRangeFilter()
|
time = django_filters.DateTimeFromToRangeFilter()
|
||||||
|
changed_object_type = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
|
||||||
'object_repr',
|
'object_repr',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -361,8 +361,8 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
|||||||
api_url='/api/users/users/',
|
api_url='/api/users/users/',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
changed_object_type = forms.ModelChoiceField(
|
changed_object_type_id = forms.ModelChoiceField(
|
||||||
queryset=ContentType.objects.order_by('model'),
|
queryset=ContentType.objects.order_by('app_label', 'model'),
|
||||||
required=False,
|
required=False,
|
||||||
widget=ContentTypeSelect(),
|
widget=ContentTypeSelect(),
|
||||||
label='Object Type'
|
label='Object Type'
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
||||||
|
from extras.choices import ObjectChangeActionChoices
|
||||||
from extras.filters import *
|
from extras.filters import *
|
||||||
from extras.models import ConfigContext, ExportTemplate, ImageAttachment, Tag
|
from extras.models import ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, Tag
|
||||||
|
from ipam.models import IPAddress
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@ -298,4 +303,98 @@ class TagTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
# TODO: ObjectChangeFilter test
|
class ObjectChangeTestCase(TestCase):
|
||||||
|
queryset = ObjectChange.objects.all()
|
||||||
|
filterset = ObjectChangeFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
users = (
|
||||||
|
User(username='user1'),
|
||||||
|
User(username='user2'),
|
||||||
|
User(username='user3'),
|
||||||
|
)
|
||||||
|
User.objects.bulk_create(users)
|
||||||
|
|
||||||
|
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||||
|
ipaddress = IPAddress.objects.create(address='192.0.2.1/24')
|
||||||
|
|
||||||
|
object_changes = (
|
||||||
|
ObjectChange(
|
||||||
|
user=users[0],
|
||||||
|
user_name=users[0].username,
|
||||||
|
request_id=uuid.uuid4(),
|
||||||
|
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||||
|
changed_object=site,
|
||||||
|
object_repr=str(site),
|
||||||
|
object_data={'name': site.name, 'slug': site.slug}
|
||||||
|
),
|
||||||
|
ObjectChange(
|
||||||
|
user=users[0],
|
||||||
|
user_name=users[0].username,
|
||||||
|
request_id=uuid.uuid4(),
|
||||||
|
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||||
|
changed_object=site,
|
||||||
|
object_repr=str(site),
|
||||||
|
object_data={'name': site.name, 'slug': site.slug}
|
||||||
|
),
|
||||||
|
ObjectChange(
|
||||||
|
user=users[1],
|
||||||
|
user_name=users[1].username,
|
||||||
|
request_id=uuid.uuid4(),
|
||||||
|
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||||
|
changed_object=site,
|
||||||
|
object_repr=str(site),
|
||||||
|
object_data={'name': site.name, 'slug': site.slug}
|
||||||
|
),
|
||||||
|
ObjectChange(
|
||||||
|
user=users[1],
|
||||||
|
user_name=users[1].username,
|
||||||
|
request_id=uuid.uuid4(),
|
||||||
|
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||||
|
changed_object=ipaddress,
|
||||||
|
object_repr=str(ipaddress),
|
||||||
|
object_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||||
|
),
|
||||||
|
ObjectChange(
|
||||||
|
user=users[2],
|
||||||
|
user_name=users[2].username,
|
||||||
|
request_id=uuid.uuid4(),
|
||||||
|
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||||
|
changed_object=ipaddress,
|
||||||
|
object_repr=str(ipaddress),
|
||||||
|
object_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||||
|
),
|
||||||
|
ObjectChange(
|
||||||
|
user=users[2],
|
||||||
|
user_name=users[2].username,
|
||||||
|
request_id=uuid.uuid4(),
|
||||||
|
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||||
|
changed_object=ipaddress,
|
||||||
|
object_repr=str(ipaddress),
|
||||||
|
object_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ObjectChange.objects.bulk_create(object_changes)
|
||||||
|
|
||||||
|
def test_id(self):
|
||||||
|
params = {'id': self.queryset.values_list('pk', flat=True)[:3]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
# def test_user(self):
|
||||||
|
# params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
|
||||||
|
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
# params = {'user': ['user1', 'user2']}
|
||||||
|
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_user_name(self):
|
||||||
|
params = {'user_name': ['user1', 'user2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_changed_object_type(self):
|
||||||
|
params = {'changed_object_type': 'dcim.site'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
|
def test_changed_object_type_id(self):
|
||||||
|
params = {'changed_object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
from django_filters.constants import EMPTY_VALUES
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dcim.forms import MACAddressField
|
from dcim.forms import MACAddressField
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -115,6 +116,26 @@ class NumericArrayFilter(django_filters.NumberFilter):
|
|||||||
return super().filter(qs, value)
|
return super().filter(qs, value)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeFilter(django_filters.CharFilter):
|
||||||
|
"""
|
||||||
|
Allow specifying a ContentType by <app_label>.<model> (e.g. "dcim.site").
|
||||||
|
"""
|
||||||
|
def filter(self, qs, value):
|
||||||
|
if value in EMPTY_VALUES:
|
||||||
|
return qs
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_label, model = value.lower().split('.')
|
||||||
|
except ValueError:
|
||||||
|
return qs.none()
|
||||||
|
return qs.filter(
|
||||||
|
**{
|
||||||
|
f'{self.field_name}__app_label': app_label,
|
||||||
|
f'{self.field_name}__model': model
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# FilterSets
|
# FilterSets
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user