Closes #4897: Allow filtering by content type identified as <app>.<model> string

This commit is contained in:
Jeremy Stretch 2020-09-22 16:06:38 -04:00
parent 5ba4252388
commit 0c3fafdfef
5 changed files with 132 additions and 7 deletions

View File

@ -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`.

View File

@ -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',
]

View File

@ -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'

View File

@ -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)

View File

@ -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
#