mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -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
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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
|
||||
@ -57,5 +58,7 @@ http://netbox/api/dcim/sites/ \
|
||||
* dcim.VirtualChassis: Added `custom_fields`
|
||||
* extras.ExportTemplate: The `template_language` field has been removed
|
||||
* 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
|
||||
* 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 tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet
|
||||
from utilities.filters import BaseFilterSet, ContentTypeFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, JobResult, ObjectChange, Tag
|
||||
@ -81,10 +81,11 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
content_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type', 'object_id', 'name']
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
@ -234,11 +235,12 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
label='Search',
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
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',
|
||||
]
|
||||
|
||||
|
@ -361,8 +361,8 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
)
|
||||
changed_object_type = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.order_by('model'),
|
||||
changed_object_type_id = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.order_by('app_label', 'model'),
|
||||
required=False,
|
||||
widget=ContentTypeSelect(),
|
||||
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.test import TestCase
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
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 virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@ -298,4 +303,98 @@ class TagTestCase(TestCase):
|
||||
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
|
||||
from django_filters.constants import EMPTY_VALUES
|
||||
from copy import deepcopy
|
||||
from dcim.forms import MACAddressField
|
||||
from django import forms
|
||||
@ -115,6 +116,26 @@ class NumericArrayFilter(django_filters.NumberFilter):
|
||||
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
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user