diff --git a/awx/api/generics.py b/awx/api/generics.py index dddd9d9e6f..17d1bdd55e 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -63,7 +63,6 @@ __all__ = [ 'SubDetailAPIView', 'ResourceAccessList', 'ParentMixin', - 'DeleteLastUnattachLabelMixin', 'SubListAttachDetachAPIView', 'CopyAPIView', 'BaseUsersList', @@ -775,28 +774,6 @@ class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView): return {'id': None} -class DeleteLastUnattachLabelMixin(object): - """ - Models for which you want the last instance to be deleted from the database - when the last disassociate is called should inherit from this class. Further, - the model should implement is_detached() - """ - - def unattach(self, request, *args, **kwargs): - (sub_id, res) = super(DeleteLastUnattachLabelMixin, self).unattach_validate(request) - if res: - return res - - res = super(DeleteLastUnattachLabelMixin, self).unattach_by_id(request, sub_id) - - obj = self.model.objects.get(id=sub_id) - - if obj.is_detached(): - obj.delete() - - return res - - class SubDetailAPIView(ParentMixin, generics.RetrieveAPIView, GenericAPIView): pass diff --git a/awx/api/urls/label.py b/awx/api/urls/label.py index 5fc0a4f629..f7158275ae 100644 --- a/awx/api/urls/label.py +++ b/awx/api/urls/label.py @@ -3,7 +3,7 @@ from django.urls import re_path -from awx.api.views import LabelList, LabelDetail +from awx.api.views.labels import LabelList, LabelDetail urls = [re_path(r'^$', LabelList.as_view(), name='label_list'), re_path(r'^(?P[0-9]+)/$', LabelDetail.as_view(), name='label_detail')] diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 54f195ec45..1a43a0bc2d 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -69,7 +69,6 @@ from awx.api.generics import ( APIView, BaseUsersList, CopyAPIView, - DeleteLastUnattachLabelMixin, GenericAPIView, ListAPIView, ListCreateAPIView, @@ -86,6 +85,7 @@ from awx.api.generics import ( SubListCreateAttachDetachAPIView, SubListDestroyAPIView, ) +from awx.api.views.labels import LabelSubListCreateAttachDetachView from awx.api.versioning import reverse from awx.main import models from awx.main.utils import ( @@ -209,26 +209,6 @@ def api_exception_handler(exc, context): return exception_handler(exc, context) -class LabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView): - def post(self, request, *args, **kwargs): - # If a label already exists in the database, attach it instead of erroring out - # that it already exists - if not getattr(self, 'label_filter', None): - return Response(dict(msg=_('Class {} missing label filter.'.format(self.__class__.__name__))), status=status.HTTP_400_BAD_REQUEST) - if 'id' not in request.data and 'name' in request.data and 'organization' in request.data: - existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization']) - if existing.exists(): - existing = existing[0] - request.data['id'] = existing.id - del request.data['name'] - del request.data['organization'] - if models.Label.objects.filter(**{self.label_filter: self.kwargs['pk']}).count() > 100: - return Response( - dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST - ) - return super().post(request, *args, **kwargs) - - class DashboardView(APIView): deprecated = True @@ -638,13 +618,9 @@ class ScheduleCredentialsList(LaunchConfigCredentialsBase): parent_model = models.Schedule -class ScheduleLabelsList(LabelList): +class ScheduleLabelsList(LabelSubListCreateAttachDetachView): - model = models.Label - serializer_class = serializers.LabelSerializer parent_model = models.Schedule - relationship = 'labels' - label_filter = 'schedule_labels' class ScheduleInstanceGroupList(SubListAttachDetachAPIView): @@ -2757,13 +2733,9 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView): return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created) -class JobTemplateLabelList(LabelList): +class JobTemplateLabelList(LabelSubListCreateAttachDetachView): - model = models.Label - serializer_class = serializers.LabelSerializer parent_model = models.JobTemplate - relationship = 'labels' - label_filter = 'unifiedjobtemplate_labels' class JobTemplateCallback(GenericAPIView): @@ -2989,13 +2961,12 @@ class WorkflowJobNodeCredentialsList(SubListAPIView): relationship = 'credentials' -class WorkflowJobNodeLabelsList(LabelList): +class WorkflowJobNodeLabelsList(SubListAPIView): model = models.Label serializer_class = serializers.LabelSerializer parent_model = models.WorkflowJobNode relationship = 'labels' - label_filter = 'workflowjobnode_labels' class WorkflowJobNodeInstanceGroupsList(SubListAttachDetachAPIView): @@ -3024,13 +2995,9 @@ class WorkflowJobTemplateNodeCredentialsList(LaunchConfigCredentialsBase): parent_model = models.WorkflowJobTemplateNode -class WorkflowJobTemplateNodeLabelsList(LabelList): +class WorkflowJobTemplateNodeLabelsList(LabelSubListCreateAttachDetachView): - model = models.Label - serializer_class = serializers.LabelSerializer parent_model = models.WorkflowJobTemplateNode - relationship = 'labels' - label_filter = 'workflowjobtemplatenode_labels' class WorkflowJobTemplateNodeInstanceGroupsList(SubListAttachDetachAPIView): @@ -4498,18 +4465,6 @@ class NotificationDetail(RetrieveAPIView): serializer_class = serializers.NotificationSerializer -class LabelList(ListCreateAPIView): - - model = models.Label - serializer_class = serializers.LabelSerializer - - -class LabelDetail(RetrieveUpdateAPIView): - - model = models.Label - serializer_class = serializers.LabelSerializer - - class ActivityStreamList(SimpleListAPIView): model = models.ActivityStream diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index 65e59790ac..31b9cf23ae 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -18,8 +18,6 @@ from rest_framework import status # AWX from awx.main.models import ActivityStream, Inventory, JobTemplate, Role, User, InstanceGroup, InventoryUpdateEvent, InventoryUpdate -from awx.main.models.label import Label - from awx.api.generics import ( ListCreateAPIView, RetrieveUpdateDestroyAPIView, @@ -27,9 +25,8 @@ from awx.api.generics import ( SubListAttachDetachAPIView, ResourceAccessList, CopyAPIView, - DeleteLastUnattachLabelMixin, - SubListCreateAttachDetachAPIView, ) +from awx.api.views.labels import LabelSubListCreateAttachDetachView from awx.api.serializers import ( @@ -39,7 +36,6 @@ from awx.api.serializers import ( InstanceGroupSerializer, InventoryUpdateEventSerializer, JobTemplateSerializer, - LabelSerializer, ) from awx.api.views.mixin import RelatedJobsPreventDeleteMixin @@ -157,28 +153,9 @@ class InventoryJobTemplateList(SubListAPIView): return qs.filter(inventory=parent) -class InventoryLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView, SubListAPIView): +class InventoryLabelList(LabelSubListCreateAttachDetachView): - model = Label - serializer_class = LabelSerializer parent_model = Inventory - relationship = 'labels' - - def post(self, request, *args, **kwargs): - # If a label already exists in the database, attach it instead of erroring out - # that it already exists - if 'id' not in request.data and 'name' in request.data and 'organization' in request.data: - existing = Label.objects.filter(name=request.data['name'], organization_id=request.data['organization']) - if existing.exists(): - existing = existing[0] - request.data['id'] = existing.id - del request.data['name'] - del request.data['organization'] - if Label.objects.filter(inventory_labels=self.kwargs['pk']).count() > 100: - return Response( - dict(msg=_('Maximum number of labels for {} reached.'.format(self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST - ) - return super(InventoryLabelList, self).post(request, *args, **kwargs) class InventoryCopy(CopyAPIView): diff --git a/awx/api/views/labels.py b/awx/api/views/labels.py new file mode 100644 index 0000000000..95a7f42941 --- /dev/null +++ b/awx/api/views/labels.py @@ -0,0 +1,71 @@ +# AWX +from awx.api.generics import SubListCreateAttachDetachAPIView, RetrieveUpdateAPIView, ListCreateAPIView +from awx.main.models import Label +from awx.api.serializers import LabelSerializer + +# Django +from django.utils.translation import gettext_lazy as _ + +# Django REST Framework +from rest_framework.response import Response +from rest_framework.status import HTTP_400_BAD_REQUEST + + +class LabelSubListCreateAttachDetachView(SubListCreateAttachDetachAPIView): + """ + For related labels lists like /api/v2/inventories/N/labels/ + + We want want the last instance to be deleted from the database + when the last disassociate happens. + + Subclasses need to define parent_model + """ + + model = Label + serializer_class = LabelSerializer + relationship = 'labels' + + def unattach(self, request, *args, **kwargs): + (sub_id, res) = super().unattach_validate(request) + if res: + return res + + res = super().unattach_by_id(request, sub_id) + + obj = self.model.objects.get(id=sub_id) + + if obj.is_detached(): + obj.delete() + + return res + + def post(self, request, *args, **kwargs): + # If a label already exists in the database, attach it instead of erroring out + # that it already exists + if 'id' not in request.data and 'name' in request.data and 'organization' in request.data: + existing = Label.objects.filter(name=request.data['name'], organization_id=request.data['organization']) + if existing.exists(): + existing = existing[0] + request.data['id'] = existing.id + del request.data['name'] + del request.data['organization'] + + # Give a 400 error if we have attached too many labels to this object + label_filter = self.parent_model._meta.get_field(self.relationship).remote_field.name + if Label.objects.filter(**{label_filter: self.kwargs['pk']}).count() > 100: + return Response(dict(msg=_(f'Maximum number of labels for {self.parent_model._meta.verbose_name_raw} reached.')), status=HTTP_400_BAD_REQUEST) + + return super().post(request, *args, **kwargs) + + +class LabelDetail(RetrieveUpdateAPIView): + + model = Label + serializer_class = LabelSerializer + + +class LabelList(ListCreateAPIView): + + name = _("Labels") + model = Label + serializer_class = LabelSerializer diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 7f69c816e7..e2a482e1dc 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -59,7 +59,7 @@ class TestApiRootView: class TestJobTemplateLabelList: def test_inherited_mixin_unattach(self): - with mock.patch('awx.api.generics.DeleteLastUnattachLabelMixin.unattach') as mixin_unattach: + with mock.patch('awx.api.views.labels.LabelSubListCreateAttachDetachView.unattach') as mixin_unattach: view = JobTemplateLabelList() mock_request = mock.MagicMock()