[PATCH v3 5/5] REST: Add submission relations
Mete Polat
metepolat2000 at gmail.com
Mon Oct 21 05:57:12 AEDT 2019
View relations or add/update/delete them as a maintainer. Maintainers
can only create relations of submissions (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 <metepolat2000 at gmail.com>
---
Previously it was possible to use the PatchSerializer. As we expanded the
support to submissions in general, it isn't a simple task anymore 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 cd33291..66eecdd 100644
--- a/docs/api/schemas/latest/patchwork.yaml
+++ b/docs/api/schemas/latest/patchwork.yaml
@@ -1032,6 +1032,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.
@@ -1307,6 +1477,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
@@ -1351,6 +1533,11 @@ components:
type: string
format: uri
readOnly: true
+ relations:
+ title: Relations URL
+ type: string
+ format: uri
+ readOnly: true
Bundle:
required:
- name
@@ -1929,6 +2116,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:
@@ -2119,6 +2314,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:
@@ -2530,6 +2741,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 6116578..6050b7d 100644
--- a/docs/api/schemas/patchwork.j2
+++ b/docs/api/schemas/patchwork.j2
@@ -1037,6 +1037,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.
@@ -1314,6 +1486,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
@@ -1358,6 +1544,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
@@ -1961,6 +2154,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:
@@ -2157,6 +2360,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:
@@ -2582,6 +2803,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 30cadcc..116ee64 100644
--- a/docs/api/schemas/v1.2/patchwork.yaml
+++ b/docs/api/schemas/v1.2/patchwork.yaml
@@ -1032,6 +1032,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.
@@ -1307,6 +1477,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
@@ -1351,6 +1533,11 @@ components:
type: string
format: uri
readOnly: true
+ relations:
+ title: Relations URL
+ type: string
+ format: uri
+ readOnly: true
Bundle:
required:
- name
@@ -1929,6 +2116,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:
@@ -2119,6 +2314,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:
@@ -2530,6 +2741,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()
+
+ at 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.23.0
More information about the Patchwork
mailing list