# Patchwork - automated patch tracking system # Copyright (C) 2017 Stephen Finucane # # SPDX-License-Identifier: GPL-2.0-or-later from django.db.models import Q from rest_framework import exceptions from rest_framework.generics import ListCreateAPIView from rest_framework.generics import RetrieveUpdateDestroyAPIView from rest_framework import permissions from rest_framework.serializers import SerializerMethodField from rest_framework.serializers import ValidationError from patchwork.api.base import BaseHyperlinkedModelSerializer from patchwork.api.base import PatchworkPermission from patchwork.api.filters import BundleFilterSet from patchwork.api.embedded import PatchSerializer from patchwork.api.embedded import ProjectSerializer from patchwork.api.embedded import UserSerializer from patchwork.api import utils from patchwork.models import Bundle class BundlePermission(permissions.BasePermission): """Ensure the API version, if configured, is >= v1.2. Bundle creation/updating was only added in API v1.2 and we don't want to change behavior in older API versions. """ def has_permission(self, request, view): # read-only permission for everything if request.method in permissions.SAFE_METHODS: return True if not utils.has_version(request, '1.2'): raise exceptions.MethodNotAllowed(request.method) if request.method == 'POST' and ( not request.user or not request.user.is_authenticated ): return False # we have more to do but we can't do that until we have an object return True def has_object_permission(self, request, view, obj): if ( request.user and request.user.is_authenticated and request.user == obj.owner ): return True if not obj.public: # if the bundle isn't public, we don't want to leak the fact that # it exists raise exceptions.NotFound return request.method in permissions.SAFE_METHODS class BundleSerializer(BaseHyperlinkedModelSerializer): web_url = SerializerMethodField() project = ProjectSerializer(read_only=True) mbox = SerializerMethodField() owner = UserSerializer(read_only=True) patches = PatchSerializer( many=True, required=True, style={'base_template': 'input.html'} ) def get_web_url(self, instance): request = self.context.get('request') return request.build_absolute_uri(instance.get_absolute_url()) def get_mbox(self, instance): request = self.context.get('request') return request.build_absolute_uri(instance.get_mbox_url()) def create(self, validated_data): patches = validated_data.pop('patches') instance = super(BundleSerializer, self).create(validated_data) instance.overwrite_patches(patches) return instance def update(self, instance, validated_data): patches = validated_data.pop('patches', None) instance = super(BundleSerializer, self).update( instance, validated_data ) if patches: instance.overwrite_patches(patches) return instance def validate_patches(self, value): if not len(value): raise ValidationError('Bundles cannot be empty') if len(set([p.project.id for p in value])) > 1: raise ValidationError( 'Bundle patches must belong to the same project' ) return value def validate(self, data): if data.get('patches'): data['project'] = data['patches'][0].project return super(BundleSerializer, self).validate(data) class Meta: model = Bundle fields = ( 'id', 'url', 'web_url', 'project', 'name', 'owner', 'patches', 'public', 'mbox', ) read_only_fields = ('project', 'owner', 'mbox') versioned_fields = { '1.1': ('web_url',), } extra_kwargs = { 'url': {'view_name': 'api-bundle-detail'}, } class BundleMixin(object): permission_classes = [PatchworkPermission & BundlePermission] serializer_class = BundleSerializer def get_queryset(self): if self.request.user.is_authenticated: bundle_filter = Q(owner=self.request.user) | Q(public=True) else: bundle_filter = Q(public=True) return ( Bundle.objects.filter(bundle_filter) .prefetch_related( 'patches', ) .select_related('owner', 'project') ) class BundleList(BundleMixin, ListCreateAPIView): """List or create bundles.""" filter_class = filterset_class = BundleFilterSet search_fields = ('name',) ordering_fields = ('id', 'name', 'owner') ordering = 'id' def perform_create(self, serializer): serializer.save(owner=self.request.user) class BundleDetail(BundleMixin, RetrieveUpdateDestroyAPIView): """ get: Show a bundle. patch: Update a bundle. put: Update a bundle. delete: Delete a bundle. """