Repository: ansible/awx
File: awx/api/generics.py (Python)

Code before:
            res = Response(data, status=status.HTTP_400_BAD_REQUEST)
        return (sub_id, res)

    def unattach_by_id(self, request, sub_id):
        parent = self.get_parent_object()
        parent_key = getattr(self, 'parent_key', None)
        relationship = getattrd(parent, self.relationship)
        sub = get_object_or_400(self.model, pk=sub_id)

        if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship, request.data):
            raise PermissionDenied()

        # Verify that removing the relationship is valid.
        unattach_errors = self.is_valid_removal(parent, sub)
        if unattach_errors is not None:
            return Response(unattach_errors, status=status.HTTP_400_BAD_REQUEST)

        if parent_key:
            sub.delete()
        else:
            with transaction.atomic():
                cursor = get_connection().cursor()
                # lock the table to prevent any race conditions at the database level
                # a transaction will automatically release the lock once committed (or rolled back) and
                # other transactions will automatically wait for the lock to be released
                cursor.execute(f'LOCK TABLE {relationship.through._meta.db_table} IN SHARE ROW EXCLUSIVE MODE')
                try:
                    relationship.remove(sub)
                finally:
                    cursor.close()

        return Response(status=status.HTTP_204_NO_CONTENT)

    def unattach(self, request, *args, **kwargs):
        (sub_id, res) = self.unattach_validate(request)
        if res:
            return res
        return self.unattach_by_id(request, sub_id)

    def post(self, request, *args, **kwargs):
        if not isinstance(request.data, dict):
            return Response('invalid type for post data', status=status.HTTP_400_BAD_REQUEST)
        if 'disassociate' in request.data:
            return self.unattach(request, *args, **kwargs)
        else:
            return self.attach(request, *args, **kwargs)


class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
    """
    Derived version of SubListCreateAttachDetachAPIView that prohibits creation

Review comment (suggestion):
Does `select_for_update` (row level lock) fix the problem? Looking for a lesser lock than a full table lock.

Code after:

    def unattach_by_id(self, request, sub_id):
        parent = self.get_parent_object()
        parent_key = getattr(self, 'parent_key', None)
        relationship = getattrd(parent, self.relationship)
        sub = get_object_or_400(self.model, pk=sub_id)

        if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship, request.data):
            raise PermissionDenied()

        # Verify that removing the relationship is valid.
        unattach_errors = self.is_valid_removal(parent, sub)
        if unattach_errors is not None:
            return Response(unattach_errors, status=status.HTTP_400_BAD_REQUEST)

        if parent_key:
            sub.delete()
        else:
            relationship.remove(sub)

        return Response(status=status.HTTP_204_NO_CONTENT)

    def unattach(self, request, *args, **kwargs):
        (sub_id, res) = self.unattach_validate(request)
        if res:
            return res
        return self.unattach_by_id(request, sub_id)

    def post(self, request, *args, **kwargs):
        if not isinstance(request.data, dict):
            return Response('invalid type for post data', status=status.HTTP_400_BAD_REQUEST)
        if 'disassociate' in request.data:
            return self.unattach(request, *args, **kwargs)
        else:
            return self.attach(request, *args, **kwargs)


class SubListAttachDetachAPIView(SubListCreateAttachDetachAPIView):
    """
    Derived version of SubListCreateAttachDetachAPIView that prohibits creation
    """

    metadata_class = SublistAttachDetatchMetadata

    def post(self, request, *args, **kwargs):
        sub_id = request.data.get('id', None)
        if not sub_id:
            return Response(dict(msg=_("{} 'id' field is missing.".format(self.model._meta.verbose_name.title()))), status=status.HTTP_400_BAD_REQUEST)
        return super(SubListAttachDetachAPIView, self).post(request, *args, **kwargs)

    def update_raw_data(self, data):