[PATCH 4/4] REST: Add submission relations
Mete Polat
metepolat2000 at gmail.com
Fri Jan 3 04:23:22 AEDT 2020
On 30.12.19 21:28, Lukas Bulwahn wrote:
> On Mo., 30. Dez. 2019 at 11:41, Mete Polat <metepolat2000 at gmail.com> wrote:
>
>> Hi Stephen,
>>
>> On 27.12.19 18:48, Stephen Finucane wrote:
>>> On Sat, 2019-12-07 at 17:46 +0100, Mete Polat wrote:
>>>> View relations and 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>/
>>>>
>>>> Co-authored-by: Daniel Axtens <dja at axtens.net>
>>>> Signed-off-by: Mete Polat <metepolat2000 at gmail.com>
>>>
>>> Why did you choose to expose this as a separate API rather than as a
>>> field on the '/patches' resource? While a 'Series' objet has enough
>>> separate metadata to warrant a separate '/series' resource, a
>>> 'SubmissionRelation' object is basically just a container. Including a
>>> 'related_patches' field on the detailed patch view would seem like more
>>> than enough detail for me, anyway, and unless there's a reason not to
>>> do this, I'd like to see it done that way. Is it possible?
>>>
>>
>> The first version of the series supported bulk creating/updating of
>> relations which was only possible by moving relations into their own url
>> [1]. As we deciced against bulk operations, I aggree that exposing a
>> related_patches field is the better choice now.
>>
>
>
> Mete, Stephen's proposal here is a simple quick refactoring of exposing
> this API, right?
> Could we get that change as a quick small v5 patch series for v2.2.0 ready?
>
It's a small refractroring on the model site (patch 02/04) but not
really on the REST API. The Event API has to be extended and the tests +
permission model have to be adapted again as well. Unfortunately I won't
be available for this Lukas.
Best regards,
Mete
> Lukas
>
>
>> Best regards,
>>
>> Mete
>>
>> [1] Or allow bulk operations on /api/patch/ in general.
>>
>>> Stephen
>>>
>>> PS: I could have sworn I had asked this before, but I can't find any
>>> mails about it so maybe I didn't. Please tell me to RTML (read the
>>> mailing list) if so
>>>
>>>> ---
>>>> Optimize db queries:
>>>> I have spent quite a lot of time in optimizing the db queries for the
>> REST API
>>>> (thanks for the tip with the Django toolbar). Daniel stated that
>>>> prefetch_related is possibly hitting the database for every relation
>> when
>>>> prefetching submissions but it turns out that we can tell Django to
>> use a
>>>> statement like:
>>>> SELECT *
>>>> FROM `patchwork_patch`
>>>> INNER JOIN `patchwork_submission`
>>>> ON (`patchwork_patch`.`submission_ptr_id` =
>> `patchwork_submission`.`id`)
>>>> WHERE `patchwork_patch`.`submission_ptr_id` IN
>> (LIST_OF_ALL_SUBMISSION_IDS)
>>>>
>>>> We do the same for `patchwork_coverletter`.
>>>> This means we only hit the db two times for casting _all_
>> submissions to a
>>>> patch or cover-letter.
>>>>
>>>> Prefetching submissions__project eliminates similar and duplicate
>> queries
>>>> that are used to determine whether a logged in user is at least
>> maintainer
>>>> of one submission's project.
>>>>
>>>> docs/api/schemas/latest/patchwork.yaml | 273 +++++++++++++++++
>>>> docs/api/schemas/patchwork.j2 | 285 ++++++++++++++++++
>>>> docs/api/schemas/v1.2/patchwork.yaml | 273 +++++++++++++++++
>>>> patchwork/api/embedded.py | 39 +++
>>>> patchwork/api/index.py | 1 +
>>>> patchwork/api/relation.py | 121 ++++++++
>>>> patchwork/models.py | 6 +
>>>> patchwork/tests/api/test_relation.py | 181 +++++++++++
>>>> patchwork/tests/utils.py | 15 +
>>>> patchwork/urls.py | 11 +
>>>> ...submission-relations-c96bb6c567b416d8.yaml | 10 +
>>>> 11 files changed, 1215 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 a5e235be936d..7dd24fd700d5 100644
>>>> --- a/docs/api/schemas/latest/patchwork.yaml
>>>> +++ b/docs/api/schemas/latest/patchwork.yaml
>>>> @@ -1039,6 +1039,188 @@ 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/Error'
>>>> + '403':
>>>> + description: Forbidden
>>>> + content:
>>>> + application/json:
>>>> + schema:
>>>> + $ref: '#/components/schemas/Error'
>>>> + '404':
>>>> + description: Not found
>>>> + content:
>>>> + application/json:
>>>> + schema:
>>>> + $ref: '#/components/schemas/Error'
>>>> + '409':
>>>> + description: Conflict
>>>> + 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'
>>>> + '409':
>>>> + description: Conflict
>>>> + 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/Error'
>>>> + '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/Error'
>>>> + '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.
>>>> @@ -1314,6 +1496,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
>>>> @@ -1358,6 +1552,11 @@ components:
>>>> type: string
>>>> format: uri
>>>> readOnly: true
>>>> + relations:
>>>> + title: Relations URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> Bundle:
>>>> required:
>>>> - name
>>>> @@ -1943,6 +2142,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:
>>>> @@ -2133,6 +2340,30 @@ 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
>>>> + by:
>>>> + type: object
>>>> + title: By
>>>> + readOnly: true
>>>> + allOf:
>>>> + - $ref: '#/components/schemas/UserEmbedded'
>>>> + submissions:
>>>> + title: Submissions
>>>> + type: array
>>>> + items:
>>>> + $ref: '#/components/schemas/SubmissionEmbedded'
>>>> + readOnly: true
>>>> + uniqueItems: true
>>>> User:
>>>> type: object
>>>> properties:
>>>> @@ -2211,6 +2442,48 @@ components:
>>>> maxLength: 255
>>>> minLength: 1
>>>> readOnly: true
>>>> + SubmissionEmbedded:
>>>> + type: object
>>>> + properties:
>>>> + id:
>>>> + title: ID
>>>> + type: integer
>>>> + readOnly: true
>>>> + url:
>>>> + title: URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> + web_url:
>>>> + title: Web URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> + msgid:
>>>> + title: Message ID
>>>> + type: string
>>>> + readOnly: true
>>>> + minLength: 1
>>>> + list_archive_url:
>>>> + title: List archive URL
>>>> + type: string
>>>> + readOnly: true
>>>> + nullable: true
>>>> + date:
>>>> + title: Date
>>>> + type: string
>>>> + format: iso8601
>>>> + readOnly: true
>>>> + name:
>>>> + title: Name
>>>> + type: string
>>>> + readOnly: true
>>>> + minLength: 1
>>>> + mbox:
>>>> + title: Mbox
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> CoverLetterEmbedded:
>>>> type: object
>>>> properties:
>>>> diff --git a/docs/api/schemas/patchwork.j2
>> b/docs/api/schemas/patchwork.j2
>>>> index 196d78466b55..a034029accf9 100644
>>>> --- a/docs/api/schemas/patchwork.j2
>>>> +++ b/docs/api/schemas/patchwork.j2
>>>> @@ -1048,6 +1048,190 @@ 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/Error'
>>>> + '403':
>>>> + description: Forbidden
>>>> + content:
>>>> + application/json:
>>>> + schema:
>>>> + $ref: '#/components/schemas/Error'
>>>> + '404':
>>>> + description: Not found
>>>> + content:
>>>> + application/json:
>>>> + schema:
>>>> + $ref: '#/components/schemas/Error'
>>>> + '409':
>>>> + description: Conflict
>>>> + 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'
>>>> + '409':
>>>> + description: Conflict
>>>> + 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/Error'
>>>> + '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/Error'
>>>> + '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.
>>>> @@ -1325,6 +1509,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
>>>> @@ -1369,6 +1567,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
>>>> @@ -1981,6 +2186,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:
>>>> @@ -2177,6 +2392,32 @@ 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
>>>> + by:
>>>> + type: object
>>>> + title: By
>>>> + readOnly: true
>>>> + allOf:
>>>> + - $ref: '#/components/schemas/UserEmbedded'
>>>> + submissions:
>>>> + title: Submissions
>>>> + type: array
>>>> + items:
>>>> + $ref: '#/components/schemas/SubmissionEmbedded'
>>>> + readOnly: true
>>>> + uniqueItems: true
>>>> +{% endif %}
>>>> User:
>>>> type: object
>>>> properties:
>>>> @@ -2255,6 +2496,50 @@ components:
>>>> maxLength: 255
>>>> minLength: 1
>>>> readOnly: true
>>>> +{% if version >= (1, 2) %}
>>>> + SubmissionEmbedded:
>>>> + type: object
>>>> + properties:
>>>> + id:
>>>> + title: ID
>>>> + type: integer
>>>> + readOnly: true
>>>> + url:
>>>> + title: URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> + web_url:
>>>> + title: Web URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> + msgid:
>>>> + title: Message ID
>>>> + type: string
>>>> + readOnly: true
>>>> + minLength: 1
>>>> + list_archive_url:
>>>> + title: List archive URL
>>>> + type: string
>>>> + readOnly: true
>>>> + nullable: true
>>>> + date:
>>>> + title: Date
>>>> + type: string
>>>> + format: iso8601
>>>> + readOnly: true
>>>> + name:
>>>> + title: Name
>>>> + type: string
>>>> + readOnly: true
>>>> + minLength: 1
>>>> + mbox:
>>>> + title: Mbox
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> +{% endif %}
>>>> CoverLetterEmbedded:
>>>> type: object
>>>> properties:
>>>> diff --git a/docs/api/schemas/v1.2/patchwork.yaml
>> b/docs/api/schemas/v1.2/patchwork.yaml
>>>> index d7b4d2957cff..99425e968881 100644
>>>> --- a/docs/api/schemas/v1.2/patchwork.yaml
>>>> +++ b/docs/api/schemas/v1.2/patchwork.yaml
>>>> @@ -1039,6 +1039,188 @@ 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/Error'
>>>> + '403':
>>>> + description: Forbidden
>>>> + content:
>>>> + application/json:
>>>> + schema:
>>>> + $ref: '#/components/schemas/Error'
>>>> + '404':
>>>> + description: Not found
>>>> + content:
>>>> + application/json:
>>>> + schema:
>>>> + $ref: '#/components/schemas/Error'
>>>> + '409':
>>>> + description: Conflict
>>>> + 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'
>>>> + '409':
>>>> + description: Conflict
>>>> + 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/Error'
>>>> + '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/Error'
>>>> + '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.
>>>> @@ -1314,6 +1496,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
>>>> @@ -1358,6 +1552,11 @@ components:
>>>> type: string
>>>> format: uri
>>>> readOnly: true
>>>> + relations:
>>>> + title: Relations URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> Bundle:
>>>> required:
>>>> - name
>>>> @@ -1943,6 +2142,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:
>>>> @@ -2133,6 +2340,30 @@ 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
>>>> + by:
>>>> + type: object
>>>> + title: By
>>>> + readOnly: true
>>>> + allOf:
>>>> + - $ref: '#/components/schemas/UserEmbedded'
>>>> + submissions:
>>>> + title: Submissions
>>>> + type: array
>>>> + items:
>>>> + $ref: '#/components/schemas/SubmissionEmbedded'
>>>> + readOnly: true
>>>> + uniqueItems: true
>>>> User:
>>>> type: object
>>>> properties:
>>>> @@ -2211,6 +2442,48 @@ components:
>>>> maxLength: 255
>>>> minLength: 1
>>>> readOnly: true
>>>> + SubmissionEmbedded:
>>>> + type: object
>>>> + properties:
>>>> + id:
>>>> + title: ID
>>>> + type: integer
>>>> + readOnly: true
>>>> + url:
>>>> + title: URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> + web_url:
>>>> + title: Web URL
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> + msgid:
>>>> + title: Message ID
>>>> + type: string
>>>> + readOnly: true
>>>> + minLength: 1
>>>> + list_archive_url:
>>>> + title: List archive URL
>>>> + type: string
>>>> + readOnly: true
>>>> + nullable: true
>>>> + date:
>>>> + title: Date
>>>> + type: string
>>>> + format: iso8601
>>>> + readOnly: true
>>>> + name:
>>>> + title: Name
>>>> + type: string
>>>> + readOnly: true
>>>> + minLength: 1
>>>> + mbox:
>>>> + title: Mbox
>>>> + type: string
>>>> + format: uri
>>>> + readOnly: true
>>>> CoverLetterEmbedded:
>>>> type: object
>>>> properties:
>>>> diff --git a/patchwork/api/embedded.py b/patchwork/api/embedded.py
>>>> index de4f31165ee7..0fba291b62b8 100644
>>>> --- a/patchwork/api/embedded.py
>>>> +++ b/patchwork/api/embedded.py
>>>> @@ -102,6 +102,45 @@ class CheckSerializer(SerializedRelatedField):
>>>> }
>>>>
>>>>
>>>> +def _upgrade_instance(instance):
>>>> + if hasattr(instance, 'patch'):
>>>> + return instance.patch
>>>> + else:
>>>> + return instance.coverletter
>>>> +
>>>> +
>>>> +class SubmissionSerializer(SerializedRelatedField):
>>>> +
>>>> + class _Serializer(BaseHyperlinkedModelSerializer):
>>>> + """We need to 'upgrade' or specialise the submission to the
>> relevant
>>>> + subclass, so we can't use the mixins. This is gross but can go
>> away
>>>> + once we flatten the models."""
>>>> + url = SerializerMethodField()
>>>> + web_url = SerializerMethodField()
>>>> + mbox = SerializerMethodField()
>>>> +
>>>> + def get_url(self, instance):
>>>> + instance = _upgrade_instance(instance)
>>>> + request = self.context.get('request')
>>>> + return
>> request.build_absolute_uri(instance.get_absolute_api_url())
>>>> +
>>>> + def get_web_url(self, instance):
>>>> + instance = _upgrade_instance(instance)
>>>> + request = self.context.get('request')
>>>> + return
>> request.build_absolute_uri(instance.get_absolute_url())
>>>> +
>>>> + def get_mbox(self, instance):
>>>> + instance = _upgrade_instance(instance)
>>>> + request = self.context.get('request')
>>>> + return request.build_absolute_uri(instance.get_mbox_url())
>>>> +
>>>> + class Meta:
>>>> + model = models.Submission
>>>> + fields = ('id', 'url', 'web_url', 'msgid',
>> 'list_archive_url',
>>>> + 'date', 'name', 'mbox')
>>>> + read_only_fields = fields
>>>> +
>>>> +
>>>> class CoverLetterSerializer(SerializedRelatedField):
>>>>
>>>> class _Serializer(MboxMixin, WebURLMixin,
>> BaseHyperlinkedModelSerializer):
>>>> diff --git a/patchwork/api/index.py b/patchwork/api/index.py
>>>> index 45485c9106f6..cf1845393835 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 000000000000..37640d62e9cc
>>>> --- /dev/null
>>>> +++ b/patchwork/api/relation.py
>>>> @@ -0,0 +1,121 @@
>>>> +# Patchwork - automated patch tracking system
>>>> +# Copyright (C) 2019, Bayerische Motoren Werke Aktiengesellschaft (BMW
>> AG)
>>>> +#
>>>> +# SPDX-License-Identifier: GPL-2.0-or-later
>>>> +
>>>> +from rest_framework import permissions
>>>> +from rest_framework import status
>>>> +from rest_framework.exceptions import PermissionDenied, APIException
>>>> +from rest_framework.generics import GenericAPIView
>>>> +from rest_framework.generics import ListCreateAPIView
>>>> +from rest_framework.generics import RetrieveUpdateDestroyAPIView
>>>> +from rest_framework.serializers import ModelSerializer
>>>> +
>>>> +from patchwork.api.base import PatchworkPermission
>>>> +from patchwork.api.embedded import SubmissionSerializer
>>>> +from patchwork.api.embedded import UserSerializer
>>>> +from patchwork.models import SubmissionRelation
>>>> +
>>>> +
>>>> +class MaintainerPermission(PatchworkPermission):
>>>> +
>>>> + def has_permission(self, request, view):
>>>> + if request.method in permissions.SAFE_METHODS:
>>>> + return True
>>>> +
>>>> + # Prevent showing an HTML POST form in the browseable API for
>> logged in
>>>> + # users who are not maintainers.
>>>> + return len(request.user.maintains) > 0
>>>> +
>>>> + def has_object_permission(self, request, view, relation):
>>>> + if request.method in permissions.SAFE_METHODS:
>>>> + return True
>>>> +
>>>> + maintains = request.user.maintains
>>>> + submissions = relation.submissions.all()
>>>> + # user has to be maintainer of every project a submission is
>> part of
>>>> + return self.check_user_maintains_all(maintains, submissions)
>>>> +
>>>> + @staticmethod
>>>> + def check_user_maintains_all(maintains, submissions):
>>>> + if any(s.project not in maintains for s in submissions):
>>>> + detail = 'At least one submission is part of a project you
>> are ' \
>>>> + 'not maintaining.'
>>>> + raise PermissionDenied(detail=detail)
>>>> + return True
>>>> +
>>>> +
>>>> +class SubmissionConflict(APIException):
>>>> + status_code = status.HTTP_409_CONFLICT
>>>> + default_detail = 'At least one submission is already part of
>> another ' \
>>>> + 'relation. You have to explicitly remove a
>> submission ' \
>>>> + 'from its existing relation before moving it to
>> this one.'
>>>> +
>>>> +
>>>> +class SubmissionRelationSerializer(ModelSerializer):
>>>> + by = UserSerializer(read_only=True)
>>>> + submissions = SubmissionSerializer(many=True)
>>>> +
>>>> + def create(self, validated_data):
>>>> + submissions = validated_data['submissions']
>>>> + if any(submission.related_id is not None
>>>> + for submission in submissions):
>>>> + raise SubmissionConflict()
>>>> + return super(SubmissionRelationSerializer,
>> self).create(validated_data)
>>>> +
>>>> + def update(self, instance, validated_data):
>>>> + submissions = validated_data['submissions']
>>>> + if any(submission.related_id is not None and
>>>> + submission.related_id != instance.id
>>>> + for submission in submissions):
>>>> + raise SubmissionConflict()
>>>> + return super(SubmissionRelationSerializer, self) \
>>>> + .update(instance, validated_data)
>>>> +
>>>> + class Meta:
>>>> + model = SubmissionRelation
>>>> + fields = ('id', 'url', 'by', 'submissions',)
>>>> + read_only_fields = ('url', 'by', )
>>>> + extra_kwargs = {
>>>> + 'url': {'view_name': 'api-relation-detail'},
>>>> + }
>>>> +
>>>> +
>>>> +class SubmissionRelationMixin(GenericAPIView):
>>>> + serializer_class = SubmissionRelationSerializer
>>>> + permission_classes = (MaintainerPermission,)
>>>> +
>>>> + def initial(self, request, *args, **kwargs):
>>>> + user = request.user
>>>> + if not hasattr(user, 'maintains'):
>>>> + if user.is_authenticated:
>>>> + user.maintains = user.profile.maintainer_projects.all()
>>>> + else:
>>>> + user.maintains = []
>>>> + super(SubmissionRelationMixin, self).initial(request, *args,
>> **kwargs)
>>>> +
>>>> + def get_queryset(self):
>>>> + return SubmissionRelation.objects.all() \
>>>> + .select_related('by') \
>>>> + .prefetch_related('submissions__patch',
>>>> + 'submissions__coverletter',
>>>> + 'submissions__project')
>>>> +
>>>> +
>>>> +class SubmissionRelationList(SubmissionRelationMixin,
>> ListCreateAPIView):
>>>> + ordering = 'id'
>>>> + ordering_fields = ['id']
>>>> +
>>>> + def perform_create(self, serializer):
>>>> + # has_object_permission() is not called when creating a new
>> relation.
>>>> + # Check whether user is maintainer of every project a
>> submission is
>>>> + # part of
>>>> + maintains = self.request.user.maintains
>>>> + submissions = serializer.validated_data['submissions']
>>>> + MaintainerPermission.check_user_maintains_all(maintains,
>> submissions)
>>>> + serializer.save(by=self.request.user)
>>>> +
>>>> +
>>>> +class SubmissionRelationDetail(SubmissionRelationMixin,
>>>> + RetrieveUpdateDestroyAPIView):
>>>> + pass
>>>> diff --git a/patchwork/models.py b/patchwork/models.py
>>>> index a92203b24ff2..9ae3370e896b 100644
>>>> --- a/patchwork/models.py
>>>> +++ b/patchwork/models.py
>>>> @@ -415,6 +415,9 @@ class CoverLetter(Submission):
>>>> kwargs={'project_id': self.project.linkname,
>>>> 'msgid': self.url_msgid})
>>>>
>>>> + def get_absolute_api_url(self):
>>>> + return reverse('api-cover-detail', kwargs={'pk': self.id})
>>>> +
>>>> def get_mbox_url(self):
>>>> return reverse('cover-mbox',
>>>> kwargs={'project_id': self.project.linkname,
>>>> @@ -604,6 +607,9 @@ class Patch(Submission):
>>>> kwargs={'project_id': self.project.linkname,
>>>> 'msgid': self.url_msgid})
>>>>
>>>> + def get_absolute_api_url(self):
>>>> + return reverse('api-patch-detail', kwargs={'pk': self.id})
>>>> +
>>>> def get_mbox_url(self):
>>>> return reverse('patch-mbox',
>>>> kwargs={'project_id': self.project.linkname,
>>>> diff --git a/patchwork/tests/api/test_relation.py
>> b/patchwork/tests/api/test_relation.py
>>>> new file mode 100644
>>>> index 000000000000..5b1a04f13670
>>>> --- /dev/null
>>>> +++ b/patchwork/tests/api/test_relation.py
>>>> @@ -0,0 +1,181 @@
>>>> +# Patchwork - automated patch tracking system
>>>> +# Copyright (C) 2019, Bayerische Motoren Werke Aktiengesellschaft (BMW
>> AG)
>>>> +#
>>>> +# SPDX-License-Identifier: GPL-2.0-or-later
>>>> +
>>>> +import unittest
>>>> +
>>>> +import six
>>>> +from django.conf import settings
>>>> +from django.urls import reverse
>>>> +
>>>> +from patchwork.tests.api import utils
>>>> +from patchwork.tests.utils import create_cover
>>>> +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:
>>>> + ANONYMOUS = 1
>>>> + NON_MAINTAINER = 2
>>>> + MAINTAINER = 3
>>>> +
>>>> +
>>>> + 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):
>>>> + """Assert post/delete/patch requests on the relation API."""
>>>> + assert method in ['post', 'delete', 'patch']
>>>> +
>>>> + # setup
>>>> +
>>>> + project = create_project()
>>>> + maintainer = create_maintainer(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
>>>> + self.client.force_authenticate(user=maintainer)
>>>> + else:
>>>> + raise ValueError
>>>> +
>>>> + resource_id = None
>>>> + req = None
>>>> +
>>>> + if method == 'delete':
>>>> + resource_id = create_relation(project=project,
>> by=maintainer).id
>>>> + elif method == 'post':
>>>> + patch_ids = [p.id for p in create_patches(2,
>> project=project)]
>>>> + req = {'submissions': patch_ids}
>>>> + elif method == 'patch':
>>>> + resource_id = create_relation(project=project,
>> by=maintainer).id
>>>> + patch_ids = [p.id for p in create_patches(2,
>> project=project)]
>>>> + req = {'submissions': patch_ids}
>>>> + else:
>>>> + raise ValueError
>>>> +
>>>> + # request
>>>> +
>>>> + resp = getattr(self.client, method)(self.api_url(resource_id),
>> req)
>>>> +
>>>> + # check
>>>> +
>>>> + self.assertEqual(expected_status, resp.status_code)
>>>> +
>>>> + if resp.status_code in range(status.HTTP_200_OK,
>>>> + status.HTTP_204_NO_CONTENT):
>>>> + self.assertRequest(req, resp.data)
>>>> +
>>>> + def assertRequest(self, request, resp):
>>>> + if request.get('id'):
>>>> + self.assertEqual(request['id'], resp['id'])
>>>> + send_ids = request['submissions']
>>>> + resp_ids = [s['id'] for s in resp['submissions']]
>>>> + six.assertCountEqual(self, resp_ids, send_ids)
>>>> +
>>>> + def assertSerialized(self, obj, resp):
>>>> + self.assertEqual(obj.id, resp['id'])
>>>> + exp_ids = [s.id for s in obj.submissions.all()]
>>>> + act_ids = [s['id'] for s in resp['submissions']]
>>>> + six.assertCountEqual(self, exp_ids, act_ids)
>>>> +
>>>> + 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-create-error-forbidden')
>>>> + def test_create_anonymous(self):
>>>> + self.request_restricted('post', UserType.ANONYMOUS)
>>>> +
>>>> + def test_create_non_maintainer(self):
>>>> + self.request_restricted('post', UserType.NON_MAINTAINER)
>>>> +
>>>> + @utils.store_samples('relation-create')
>>>> + def test_create_maintainer(self):
>>>> + self.request_restricted('post', UserType.MAINTAINER)
>>>> +
>>>> + @utils.store_samples('relation-update-error-forbidden')
>>>> + def test_update_anonymous(self):
>>>> + self.request_restricted('patch', UserType.ANONYMOUS)
>>>> +
>>>> + def test_update_non_maintainer(self):
>>>> + self.request_restricted('patch', UserType.NON_MAINTAINER)
>>>> +
>>>> + @utils.store_samples('relation-update')
>>>> + def test_update_maintainer(self):
>>>> + self.request_restricted('patch', UserType.MAINTAINER)
>>>> +
>>>> + @utils.store_samples('relation-delete-error-forbidden')
>>>> + def test_delete_anonymous(self):
>>>> + self.request_restricted('delete', UserType.ANONYMOUS)
>>>> +
>>>> + def test_delete_non_maintainer(self):
>>>> + self.request_restricted('delete', UserType.NON_MAINTAINER)
>>>> +
>>>> + @utils.store_samples('relation-update')
>>>> + def test_delete_maintainer(self):
>>>> + self.request_restricted('delete', UserType.MAINTAINER)
>>>> +
>>>> + def test_submission_conflict(self):
>>>> + project = create_project()
>>>> + maintainer = create_maintainer(project)
>>>> + self.client.force_authenticate(user=maintainer)
>>>> + relation = create_relation(by=maintainer, project=project)
>>>> + submission_ids = [s.id for s in relation.submissions.all()]
>>>> +
>>>> + # try to create a new relation with a new submission (cover)
>> and
>>>> + # submissions already bound to another relation
>>>> + cover = create_cover(project=project)
>>>> + submission_ids.append(cover.id)
>>>> + req = {'submissions': submission_ids}
>>>> + resp = self.client.post(self.api_url(), req)
>>>> + self.assertEqual(status.HTTP_409_CONFLICT, resp.status_code)
>>>> +
>>>> + # try to patch relation
>>>> + resp = self.client.patch(self.api_url(relation.id), req)
>>>> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
>>>> diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py
>>>> index 577183d0986c..ffe90976233e 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,17 @@ 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, by=None, **kwargs):
>>>> + if not by:
>>>> + project = create_project()
>>>> + kwargs['project'] = project
>>>> + by = create_maintainer(project)
>>>> + relation = SubmissionRelation.objects.create(by=by)
>>>> + values = {
>>>> + 'related': relation
>>>> + }
>>>> + values.update(kwargs)
>>>> + create_patches(count_patches, **values)
>>>> + return relation
>>>> diff --git a/patchwork/urls.py b/patchwork/urls.py
>>>> index dcdcfb49e67e..92095f62c7b9 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'),
>>>> ]
>>>>
>>>> +
More information about the Patchwork
mailing list