mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
* Closes #17793: Introduce a REST API endpoint for tagged objects * Add missing object_id filter to TaggedItemFilterSet
This commit is contained in:
parent
d1712c45bb
commit
f7fdf07949
@ -1,10 +1,16 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from extras.models import Tag
|
from extras.models import Tag, TaggedItem
|
||||||
|
from netbox.api.exceptions import SerializerNotFound
|
||||||
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
|
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import BaseModelSerializer, ValidatedModelSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'TagSerializer',
|
'TagSerializer',
|
||||||
|
'TaggedItemSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -25,3 +31,37 @@ class TagSerializer(ValidatedModelSerializer):
|
|||||||
'tagged_items', 'created', 'last_updated',
|
'tagged_items', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemSerializer(BaseModelSerializer):
|
||||||
|
object_type = ContentTypeField(
|
||||||
|
source='content_type',
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
object = serializers.SerializerMethodField(
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
tag = TagSerializer(
|
||||||
|
nested=True,
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TaggedItem
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'tag',
|
||||||
|
]
|
||||||
|
brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'object', 'tag')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField())
|
||||||
|
def get_object(self, obj):
|
||||||
|
"""
|
||||||
|
Serialize a nested representation of the tagged object.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
serializer = get_serializer_for_model(obj.content_object)
|
||||||
|
except SerializerNotFound:
|
||||||
|
return obj.object_repr
|
||||||
|
data = serializer(obj.content_object, nested=True, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
|
return data
|
||||||
|
@ -19,6 +19,7 @@ router.register('notifications', views.NotificationViewSet)
|
|||||||
router.register('notification-groups', views.NotificationGroupViewSet)
|
router.register('notification-groups', views.NotificationGroupViewSet)
|
||||||
router.register('subscriptions', views.SubscriptionViewSet)
|
router.register('subscriptions', views.SubscriptionViewSet)
|
||||||
router.register('tags', views.TagViewSet)
|
router.register('tags', views.TagViewSet)
|
||||||
|
router.register('tagged-objects', views.TaggedItemViewSet)
|
||||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||||
router.register('journal-entries', views.JournalEntryViewSet)
|
router.register('journal-entries', views.JournalEntryViewSet)
|
||||||
router.register('config-contexts', views.ConfigContextViewSet)
|
router.register('config-contexts', views.ConfigContextViewSet)
|
||||||
|
@ -6,6 +6,7 @@ from rest_framework import status
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
||||||
|
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
@ -20,7 +21,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
|||||||
from netbox.api.features import SyncedDataMixin
|
from netbox.api.features import SyncedDataMixin
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
|
||||||
from utilities.exceptions import RQWorkerNotRunningException
|
from utilities.exceptions import RQWorkerNotRunningException
|
||||||
from utilities.request import copy_safe_request
|
from utilities.request import copy_safe_request
|
||||||
from . import serializers
|
from . import serializers
|
||||||
@ -172,6 +173,12 @@ class TagViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.TagFilterSet
|
filterset_class = filtersets.TagFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemViewSet(RetrieveModelMixin, ListModelMixin, BaseViewSet):
|
||||||
|
queryset = TaggedItem.objects.prefetch_related('content_type', 'content_object', 'tag')
|
||||||
|
serializer_class = serializers.TaggedItemSerializer
|
||||||
|
filterset_class = filtersets.TaggedItemFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
@ -31,6 +31,7 @@ __all__ = (
|
|||||||
'SavedFilterFilterSet',
|
'SavedFilterFilterSet',
|
||||||
'ScriptFilterSet',
|
'ScriptFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
|
'TaggedItemFilterSet',
|
||||||
'WebhookFilterSet',
|
'WebhookFilterSet',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -492,6 +493,41 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemFilterSet(BaseFilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label=_('Search'),
|
||||||
|
)
|
||||||
|
object_type = ContentTypeFilter(
|
||||||
|
field_name='content_type'
|
||||||
|
)
|
||||||
|
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
field_name='content_type_id'
|
||||||
|
)
|
||||||
|
tag_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Tag.objects.all()
|
||||||
|
)
|
||||||
|
tag = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='tag__slug',
|
||||||
|
queryset=Tag.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TaggedItem
|
||||||
|
fields = ('id', 'object_id')
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(tag__name__icontains=value) |
|
||||||
|
Q(tag__slug__icontains=value) |
|
||||||
|
Q(tag__description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -9,6 +9,7 @@ from netbox.choices import ColorChoices
|
|||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Tag',
|
'Tag',
|
||||||
@ -72,6 +73,7 @@ class TaggedItem(GenericTaggedItemBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
_netbox_private = True
|
_netbox_private = True
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=["content_type", "object_id"])]
|
indexes = [models.Index(fields=["content_type", "object_id"])]
|
||||||
|
@ -538,6 +538,34 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|||||||
Tag.objects.bulk_create(tags)
|
Tag.objects.bulk_create(tags)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemTest(
|
||||||
|
APIViewTestCases.GetObjectViewTestCase,
|
||||||
|
APIViewTestCases.ListObjectsViewTestCase
|
||||||
|
):
|
||||||
|
model = TaggedItem
|
||||||
|
brief_fields = ['display', 'id', 'object', 'object_id', 'object_type', 'tag', 'url']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
tags = (
|
||||||
|
Tag(name='Tag 1', slug='tag-1'),
|
||||||
|
Tag(name='Tag 2', slug='tag-2'),
|
||||||
|
Tag(name='Tag 3', slug='tag-3'),
|
||||||
|
)
|
||||||
|
Tag.objects.bulk_create(tags)
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
Site(name='Site 3', slug='site-3'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
sites[0].tags.set([tags[0], tags[1]])
|
||||||
|
sites[1].tags.set([tags[1], tags[2]])
|
||||||
|
sites[2].tags.set([tags[2], tags[0]])
|
||||||
|
|
||||||
|
|
||||||
# TODO: Standardize to APIViewTestCase (needs create & update tests)
|
# TODO: Standardize to APIViewTestCase (needs create & update tests)
|
||||||
class ImageAttachmentTest(
|
class ImageAttachmentTest(
|
||||||
APIViewTestCases.GetObjectViewTestCase,
|
APIViewTestCases.GetObjectViewTestCase,
|
||||||
|
@ -1250,6 +1250,62 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemFilterSetTestCase(TestCase):
|
||||||
|
queryset = TaggedItem.objects.all()
|
||||||
|
filterset = TaggedItemFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
tags = (
|
||||||
|
Tag(name='Tag 1', slug='tag-1'),
|
||||||
|
Tag(name='Tag 2', slug='tag-2'),
|
||||||
|
Tag(name='Tag 3', slug='tag-3'),
|
||||||
|
)
|
||||||
|
Tag.objects.bulk_create(tags)
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
Site(name='Site 3', slug='site-3'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
sites[0].tags.add(tags[0])
|
||||||
|
sites[1].tags.add(tags[1])
|
||||||
|
sites[2].tags.add(tags[2])
|
||||||
|
|
||||||
|
tenants = (
|
||||||
|
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||||
|
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||||
|
Tenant(name='Tenant 3', slug='tenant-3'),
|
||||||
|
)
|
||||||
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
tenants[0].tags.add(tags[0])
|
||||||
|
tenants[1].tags.add(tags[1])
|
||||||
|
tenants[2].tags.add(tags[2])
|
||||||
|
|
||||||
|
def test_tag(self):
|
||||||
|
tags = Tag.objects.all()[:2]
|
||||||
|
params = {'tag': [tags[0].slug, tags[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'tag_id': [tags[0].pk, tags[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_object_type(self):
|
||||||
|
object_type = ObjectType.objects.get_for_model(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_id(self):
|
||||||
|
site_ids = Site.objects.values_list('pk', flat=True)
|
||||||
|
params = {
|
||||||
|
'object_type': 'dcim.site',
|
||||||
|
'object_id': site_ids[:2],
|
||||||
|
}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ChangeLoggedFilterSetTestCase(TestCase):
|
class ChangeLoggedFilterSetTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
Evaluate base ChangeLoggedFilterSet filters using the Site model.
|
Evaluate base ChangeLoggedFilterSet filters using the Site model.
|
||||||
|
Loading…
Reference in New Issue
Block a user