From: Mete Polat <metepolat2...@gmail.com> View relations or add/update/delete them as a maintainer. Maintainers can only create relations of sumbissions (patches/cover letters) which are part of a project they maintain.
New REST API urls: api/relations/ api/relations/<relation_id>/ Signed-off-by: Mete Polat <metepolat2...@gmail.com> --- Previously it was possible to use the PatchSerializer. As we expanded the support for submissions in general, it isn't a simple task anymore for showing hyperlinked submissions as Patch and CoverLetter are not flattened into one model yet. Right now only the submission ids are shown. docs/api/schemas/latest/patchwork.yaml | 218 +++++++++++++++++ docs/api/schemas/patchwork.j2 | 230 ++++++++++++++++++ docs/api/schemas/v1.2/patchwork.yaml | 218 +++++++++++++++++ patchwork/api/index.py | 1 + patchwork/api/relation.py | 73 ++++++ patchwork/tests/api/test_relation.py | 194 +++++++++++++++ patchwork/tests/utils.py | 11 + patchwork/urls.py | 11 + ...submission-relations-c96bb6c567b416d8.yaml | 10 + 9 files changed, 966 insertions(+) create mode 100644 patchwork/api/relation.py create mode 100644 patchwork/tests/api/test_relation.py create mode 100644 releasenotes/notes/add-submission-relations-c96bb6c567b416d8.yaml diff --git a/docs/api/schemas/latest/patchwork.yaml b/docs/api/schemas/latest/patchwork.yaml index 11ea4a6..7f9df72 100644 --- a/docs/api/schemas/latest/patchwork.yaml +++ b/docs/api/schemas/latest/patchwork.yaml @@ -916,6 +916,176 @@ paths: $ref: '#/components/schemas/Error' tags: - series + /api/relations/: + get: + description: List relations. + operationId: relations_list + parameters: + - $ref: '#/components/parameters/Page' + - $ref: '#/components/parameters/PageSize' + - $ref: '#/components/parameters/Order' + responses: + '200': + description: '' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Relation' + tags: + - relations + post: + description: Create a relation. + operationId: relations_create + security: + - basicAuth: [] + - apiKeyAuth: [] + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Invalid Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - checks + /api/relations/{id}/: + get: + description: Show a relation. + operationId: relation_read + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations + patch: + description: Update a relation (partial). + operationId: relations_partial_update + security: + - basicAuth: [] + - apiKeyAuth: [] + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations + put: + description: Update a relation. + operationId: relations_update + security: + - basicAuth: [] + - apiKeyAuth: [] + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations /api/users/: get: description: List users. @@ -1179,6 +1349,18 @@ components: application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/User' + Relation: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RelationUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/RelationUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/RelationUpdate' schemas: Index: type: object @@ -1223,6 +1405,11 @@ components: type: string format: uri readOnly: true + relations: + title: Relations URL + type: string + format: uri + readOnly: true Bundle: required: - name @@ -1782,6 +1969,14 @@ components: title: Delegate type: integer nullable: true + RelationUpdate: + type: object + properties: + submissions: + title: Submission IDs + type: array + items: + type: integer Person: type: object properties: @@ -1971,6 +2166,22 @@ components: $ref: '#/components/schemas/PatchEmbedded' readOnly: true uniqueItems: true + Relation: + type: object + properties: + id: + title: ID + type: integer + url: + title: URL + type: string + format: uri + readOnly: true + submissions: + type: array + items: + type: integer + uniqueItems: true User: type: object properties: @@ -2368,6 +2579,13 @@ components: type: string format: uri readOnly: true + ErrorRelation: + type: object + properties: + submissions: + type: array + items: + type: string ErrorUserUpdate: type: object properties: diff --git a/docs/api/schemas/patchwork.j2 b/docs/api/schemas/patchwork.j2 index 2b1e043..ea1aadd 100644 --- a/docs/api/schemas/patchwork.j2 +++ b/docs/api/schemas/patchwork.j2 @@ -917,6 +917,178 @@ paths: $ref: '#/components/schemas/Error' tags: - series +{% if version >= (1, 2) %} + /api/{{ version_url }}relations/: + get: + description: List relations. + operationId: relations_list + parameters: + - $ref: '#/components/parameters/Page' + - $ref: '#/components/parameters/PageSize' + - $ref: '#/components/parameters/Order' + responses: + '200': + description: '' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Relation' + tags: + - relations + post: + description: Create a relation. + operationId: relations_create + security: + - basicAuth: [] + - apiKeyAuth: [] + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Invalid Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - checks + /api/{{ version_url }}relations/{id}/: + get: + description: Show a relation. + operationId: relation_read + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations + patch: + description: Update a relation (partial). + operationId: relations_partial_update + security: + - basicAuth: [] + - apiKeyAuth: [] + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations + put: + description: Update a relation. + operationId: relations_update + security: + - basicAuth: [] + - apiKeyAuth: [] + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations +{% endif %} /api/{{ version_url }}users/: get: description: List users. @@ -1180,6 +1352,20 @@ components: application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/User' +{% if version >= (1, 2) %} + Relation: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RelationUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/RelationUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/RelationUpdate' +{% endif %} schemas: Index: type: object @@ -1224,6 +1410,13 @@ components: type: string format: uri readOnly: true +{% if version >= (1, 2) %} + relations: + title: Relations URL + type: string + format: uri + readOnly: true +{% endif %} Bundle: required: - name @@ -1803,6 +1996,16 @@ components: title: Delegate type: integer nullable: true +{% if version >= (1, 2) %} + RelationUpdate: + type: object + properties: + submissions: + title: Submission IDs + type: array + items: + type: integer +{% endif %} Person: type: object properties: @@ -1998,6 +2201,24 @@ components: $ref: '#/components/schemas/PatchEmbedded' readOnly: true uniqueItems: true +{% if version >= (1, 2) %} + Relation: + type: object + properties: + id: + title: ID + type: integer + url: + title: URL + type: string + format: uri + readOnly: true + submissions: + type: array + items: + type: integer + uniqueItems: true +{% endif %} User: type: object properties: @@ -2407,6 +2628,15 @@ components: type: string format: uri readOnly: true +{% if version >= (1, 2) %} + ErrorRelation: + type: object + properties: + submissions: + type: array + items: + type: string +{% endif %} ErrorUserUpdate: type: object properties: diff --git a/docs/api/schemas/v1.2/patchwork.yaml b/docs/api/schemas/v1.2/patchwork.yaml index 45018cc..a7d801f 100644 --- a/docs/api/schemas/v1.2/patchwork.yaml +++ b/docs/api/schemas/v1.2/patchwork.yaml @@ -916,6 +916,176 @@ paths: $ref: '#/components/schemas/Error' tags: - series + /api/1.2/relations/: + get: + description: List relations. + operationId: relations_list + parameters: + - $ref: '#/components/parameters/Page' + - $ref: '#/components/parameters/PageSize' + - $ref: '#/components/parameters/Order' + responses: + '200': + description: '' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Relation' + tags: + - relations + post: + description: Create a relation. + operationId: relations_create + security: + - basicAuth: [] + - apiKeyAuth: [] + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Invalid Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - checks + /api/1.2/relations/{id}/: + get: + description: Show a relation. + operationId: relation_read + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations + patch: + description: Update a relation (partial). + operationId: relations_partial_update + security: + - basicAuth: [] + - apiKeyAuth: [] + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations + put: + description: Update a relation. + operationId: relations_update + security: + - basicAuth: [] + - apiKeyAuth: [] + parameters: + - in: path + name: id + description: A unique integer value identifying this relation. + required: true + schema: + title: ID + type: integer + requestBody: + $ref: '#/components/requestBodies/Relation' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Relation' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorRelation' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + tags: + - relations /api/1.2/users/: get: description: List users. @@ -1179,6 +1349,18 @@ components: application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/User' + Relation: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RelationUpdate' + multipart/form-data: + schema: + $ref: '#/components/schemas/RelationUpdate' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/RelationUpdate' schemas: Index: type: object @@ -1223,6 +1405,11 @@ components: type: string format: uri readOnly: true + relations: + title: Relations URL + type: string + format: uri + readOnly: true Bundle: required: - name @@ -1782,6 +1969,14 @@ components: title: Delegate type: integer nullable: true + RelationUpdate: + type: object + properties: + submissions: + title: Submission IDs + type: array + items: + type: integer Person: type: object properties: @@ -1971,6 +2166,22 @@ components: $ref: '#/components/schemas/PatchEmbedded' readOnly: true uniqueItems: true + Relation: + type: object + properties: + id: + title: ID + type: integer + url: + title: URL + type: string + format: uri + readOnly: true + submissions: + type: array + items: + type: integer + uniqueItems: true User: type: object properties: @@ -2368,6 +2579,13 @@ components: type: string format: uri readOnly: true + ErrorRelation: + type: object + properties: + submissions: + type: array + items: + type: string ErrorUserUpdate: type: object properties: diff --git a/patchwork/api/index.py b/patchwork/api/index.py index 45485c9..cf18453 100644 --- a/patchwork/api/index.py +++ b/patchwork/api/index.py @@ -21,4 +21,5 @@ class IndexView(APIView): 'series': reverse('api-series-list', request=request), 'events': reverse('api-event-list', request=request), 'bundles': reverse('api-bundle-list', request=request), + 'relations': reverse('api-relation-list', request=request), }) diff --git a/patchwork/api/relation.py b/patchwork/api/relation.py new file mode 100644 index 0000000..e7d002b --- /dev/null +++ b/patchwork/api/relation.py @@ -0,0 +1,73 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2019, Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from django.db.models import Count +from rest_framework import permissions +from rest_framework.generics import ListCreateAPIView +from rest_framework.generics import RetrieveUpdateDestroyAPIView +from rest_framework.serializers import ModelSerializer + +from patchwork.models import SubmissionRelation + + +class MaintainerPermission(permissions.BasePermission): + + def has_object_permission(self, request, view, submissions): + if request.method in permissions.SAFE_METHODS: + return True + + user = request.user + if not user.is_authenticated: + return False + + if isinstance(submissions, SubmissionRelation): + submissions = list(submissions.submissions.all()) + maintaining = user.profile.maintainer_projects.all() + return all(s.project in maintaining for s in submissions) + + def has_permission(self, request, view): + return request.method in permissions.SAFE_METHODS or \ + (request.user.is_authenticated and + request.user.profile.maintainer_projects.count() > 0) + + +class SubmissionRelationSerializer(ModelSerializer): + class Meta: + model = SubmissionRelation + fields = ('id', 'url', 'submissions',) + read_only_fields = ('url',) + extra_kwargs = { + 'url': {'view_name': 'api-relation-detail'}, + } + + +class SubmissionRelationMixin: + serializer_class = SubmissionRelationSerializer + permission_classes = (MaintainerPermission,) + + def get_queryset(self): + return SubmissionRelation.objects.all() \ + .prefetch_related('submissions') + + +class SubmissionRelationList(SubmissionRelationMixin, ListCreateAPIView): + ordering = 'id' + ordering_fields = ['id', 'submission_count'] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + submissions = serializer.validated_data['submissions'] + self.check_object_permissions(request, submissions) + return super().create(request, *args, **kwargs) + + def get_queryset(self): + return super().get_queryset() \ + .annotate(submission_count=Count('submission')) + + +class SubmissionRelationDetail(SubmissionRelationMixin, + RetrieveUpdateDestroyAPIView): + pass diff --git a/patchwork/tests/api/test_relation.py b/patchwork/tests/api/test_relation.py new file mode 100644 index 0000000..296926d --- /dev/null +++ b/patchwork/tests/api/test_relation.py @@ -0,0 +1,194 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2019, Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import unittest +from enum import Enum +from enum import auto + +import six +from django.conf import settings +from django.urls import reverse + +from patchwork.models import SubmissionRelation +from patchwork.tests.api import utils +from patchwork.tests.utils import create_maintainer +from patchwork.tests.utils import create_patches +from patchwork.tests.utils import create_project +from patchwork.tests.utils import create_relation +from patchwork.tests.utils import create_user + +if settings.ENABLE_REST_API: + from rest_framework import status + + +class UserType(Enum): + ANONYMOUS = auto() + NON_MAINTAINER = auto() + MAINTAINER = auto() + +@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API') +class TestRelationAPI(utils.APITestCase): + fixtures = ['default_tags'] + + @staticmethod + def api_url(item=None): + kwargs = {} + if item is None: + return reverse('api-relation-list', kwargs=kwargs) + kwargs['pk'] = item + return reverse('api-relation-detail', kwargs=kwargs) + + def request_restricted(self, method, user_type: UserType): + # setup + + project = create_project() + + if user_type == UserType.ANONYMOUS: + expected_status = status.HTTP_403_FORBIDDEN + elif user_type == UserType.NON_MAINTAINER: + expected_status = status.HTTP_403_FORBIDDEN + self.client.force_authenticate(user=create_user()) + elif user_type == UserType.MAINTAINER: + if method == 'post': + expected_status = status.HTTP_201_CREATED + elif method == 'delete': + expected_status = status.HTTP_204_NO_CONTENT + else: + expected_status = status.HTTP_200_OK + user = create_maintainer(project) + self.client.force_authenticate(user=user) + else: + raise ValueError + + resource_id = None + send = None + + if method == 'delete': + resource_id = create_relation(project=project).id + elif method == 'post': + patch_ids = [p.id for p in create_patches(2, project=project)] + send = {'submissions': patch_ids} + elif method == 'patch': + resource_id = create_relation(project=project).id + patch_ids = [p.id for p in create_patches(2, project=project)] + send = {'submissions': patch_ids} + else: + raise ValueError + + # request + + resp = getattr(self.client, method)(self.api_url(resource_id), send) + + # check + + self.assertEqual(expected_status, resp.status_code) + + if resp.status_code not in range(200, 202): + return + + if resource_id: + self.assertEqual(resource_id, resp.data['id']) + + send_ids = send['submissions'] + resp_ids = resp.data['submissions'] + six.assertCountEqual(self, resp_ids, send_ids) + + def assertSerialized(self, obj: SubmissionRelation, resp: dict): + self.assertEqual(obj.id, resp['id']) + obj = [s.id for s in obj.submissions.all()] + six.assertCountEqual(self, obj, resp['submissions']) + + def test_list_empty(self): + """List relation when none are present.""" + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(0, len(resp.data)) + + @utils.store_samples('relation-list') + def test_list(self): + """List relations.""" + relation = create_relation() + + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(1, len(resp.data)) + self.assertSerialized(relation, resp.data[0]) + + def test_detail(self): + """Show relation.""" + relation = create_relation() + + resp = self.client.get(self.api_url(relation.id)) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertSerialized(relation, resp.data) + + @utils.store_samples('relation-update-error-forbidden') + def test_update_anonymous(self): + """Update relation as anonymous user. + + Ensure updates can be performed by maintainers. + """ + self.request_restricted('patch', UserType.ANONYMOUS) + + def test_update_non_maintainer(self): + """Update relation as non-maintainer. + + Ensure updates can be performed by maintainers. + """ + self.request_restricted('patch', UserType.NON_MAINTAINER) + + @utils.store_samples('relation-update') + def test_update_maintainer(self): + """Update relation as maintainer. + + Ensure updates can be performed by maintainers. + """ + self.request_restricted('patch', UserType.MAINTAINER) + + @utils.store_samples('relation-delete-error-forbidden') + def test_delete_anonymous(self): + """Delete relation as anonymous user. + + Ensure deletes can be performed by maintainers. + """ + self.request_restricted('delete', UserType.ANONYMOUS) + + def test_delete_non_maintainer(self): + """Delete relation as non-maintainer. + + Ensure deletes can be performed by maintainers. + """ + self.request_restricted('delete', UserType.NON_MAINTAINER) + + @utils.store_samples('relation-update') + def test_delete_maintainer(self): + """Delete relation as maintainer. + + Ensure deletes can be performed by maintainers. + """ + self.request_restricted('delete', UserType.MAINTAINER) + + @utils.store_samples('relation-create-error-forbidden') + def test_create_anonymous(self): + """Create relation as anonymous user. + + Ensure creates can be performed by maintainers. + """ + self.request_restricted('post', UserType.ANONYMOUS) + + def test_create_non_maintainer(self): + """Create relation as non-maintainer. + + Ensure creates can be performed by maintainers. + """ + self.request_restricted('post', UserType.NON_MAINTAINER) + + @utils.store_samples('relation-create') + def test_create_maintainer(self): + """Create relation as maintainer. + + Ensure creates can be performed by maintainers. + """ + self.request_restricted('post', UserType.MAINTAINER) diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py index 577183d..47149de 100644 --- a/patchwork/tests/utils.py +++ b/patchwork/tests/utils.py @@ -16,6 +16,7 @@ from patchwork.models import Check from patchwork.models import Comment from patchwork.models import CoverLetter from patchwork.models import Patch +from patchwork.models import SubmissionRelation from patchwork.models import Person from patchwork.models import Project from patchwork.models import Series @@ -347,3 +348,13 @@ def create_covers(count=1, **kwargs): kwargs (dict): Overrides for various cover letter fields """ return _create_submissions(create_cover, count, **kwargs) + + +def create_relation(count_patches=2, **kwargs): + relation = SubmissionRelation.objects.create() + values = { + 'related': relation + } + values.update(kwargs) + create_patches(count_patches, **values) + return relation diff --git a/patchwork/urls.py b/patchwork/urls.py index dcdcfb4..92095f6 100644 --- a/patchwork/urls.py +++ b/patchwork/urls.py @@ -187,6 +187,7 @@ if settings.ENABLE_REST_API: from patchwork.api import patch as api_patch_views # noqa from patchwork.api import person as api_person_views # noqa from patchwork.api import project as api_project_views # noqa + from patchwork.api import relation as api_relation_views # noqa from patchwork.api import series as api_series_views # noqa from patchwork.api import user as api_user_views # noqa @@ -256,9 +257,19 @@ if settings.ENABLE_REST_API: name='api-cover-comment-list'), ] + api_1_2_patterns = [ + url(r'^relations/$', + api_relation_views.SubmissionRelationList.as_view(), + name='api-relation-list'), + url(r'^relations/(?P<pk>[^/]+)/$', + api_relation_views.SubmissionRelationDetail.as_view(), + name='api-relation-detail'), + ] + urlpatterns += [ url(r'^api/(?:(?P<version>(1.0|1.1|1.2))/)?', include(api_patterns)), url(r'^api/(?:(?P<version>(1.1|1.2))/)?', include(api_1_1_patterns)), + url(r'^api/(?:(?P<version>1.2)/)?', include(api_1_2_patterns)), # token change url(r'^user/generate-token/$', user_views.generate_token, diff --git a/releasenotes/notes/add-submission-relations-c96bb6c567b416d8.yaml b/releasenotes/notes/add-submission-relations-c96bb6c567b416d8.yaml new file mode 100644 index 0000000..cb87799 --- /dev/null +++ b/releasenotes/notes/add-submission-relations-c96bb6c567b416d8.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Submissions (cover letters or patches) can now be related to other ones + (e.g. revisions). Relations can be set via the REST API by maintainers + (currently only for submissions of projects they maintain) +api: + - | + Relations are available via ``/relations/`` and + ``/relations/{relationID}/`` endpoints. -- 2.20.1 (Apple Git-117) _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork