[PATCH v3 08/10] api: add patch comments detail endpoint and respective tests
Daniel Axtens
dja at axtens.net
Tue Aug 17 00:01:42 AEST 2021
Raxel Gutierrez <raxel at google.com> writes:
> Add new endpoint for patch comments at api/.../comments/<comment_id>.
> The endpoint will make it possible to use the REST API to update the new
> `addressed` field for individual patch comments with JavaScript on the
> client side. In the process of these changes, clean up use of the
> CurrentPatchDefault context so that it exists in base.py and can be used
> throughout the API (e.g. Check and Comment REST endpoints).
I was poking around the API to check DB load.
I wondered how comments are numbered - comment ID refers, I discovered,
to the ID of the comment in the DB, it's not the n'th comment on that
patch.
That's fine, but if I go to a URL of an invalid comment I get the
following splat - the key parts of which I've highlighted at the end.
Traceback (most recent call last):
File "/home/patchwork/patchwork/patchwork/api/comment.py", line 101, in get_object
obj = queryset.get(id=int(comment_id))
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/query.py", line 429, in get
raise self.model.DoesNotExist(
During handling of the above exception (PatchComment matching query does not exist.), another exception occurred:
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
return self.dispatch(request, *args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/rest_framework/generics.py", line 252, in get
return self.retrieve(request, *args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/rest_framework/mixins.py", line 54, in retrieve
instance = self.get_object()
File "/home/patchwork/patchwork/patchwork/api/comment.py", line 103, in get_object
obj = get_object_or_404(queryset, linkname=comment_id)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/shortcuts.py", line 76, in get_object_or_404
return queryset.get(*args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/query.py", line 418, in get
clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/query.py", line 942, in filter
return self._filter_or_exclude(False, *args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/query.py", line 962, in _filter_or_exclude
clone._filter_or_exclude_inplace(negate, *args, **kwargs)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/query.py", line 969, in _filter_or_exclude_inplace
self._query.add_q(Q(*args, **kwargs))
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/sql/query.py", line 1358, in add_q
clause, _ = self._add_q(q_object, self.used_aliases)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/sql/query.py", line 1377, in _add_q
child_clause, needed_inner = self.build_filter(
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/sql/query.py", line 1258, in build_filter
lookups, parts, reffed_expression = self.solve_lookup_type(arg)
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/sql/query.py", line 1084, in solve_lookup_type
_, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
File "/opt/pyenv/versions/3.9.5/lib/python3.9/site-packages/django/db/models/sql/query.py", line 1481, in names_to_path
raise FieldError("Cannot resolve keyword '%s' into field. "
Exception Type: FieldError at /api/patches/2/comments/1/
Exception Value: Cannot resolve keyword 'linkname' into field. Choices are: addressed, content, date, headers, id, msgid, patch, patch_id, submitter, submitter_id
It looks like the part of patchwork that triggers that is
File "/home/patchwork/patchwork/patchwork/api/comment.py", line 103, in get_object
obj = get_object_or_404(queryset, linkname=comment_id)
Looking at what your code does there, I'm a bit confused... but there is
no linkname on a comment, so something should be changed.
Apart from that the DB load seems unchanged, so that's very good news.
I haven't checked back against all the comments I made last time but
looking at the diffstat and having a quick flick through it looks pretty
good.
Kind regards,
Daniel
> Add the OpenAPI definition of the new endpoint and upgrade API version
> to v1.3 to reflect the new endpoint as minor change for semantic
> versioning.
>
> Add tests for the new api/.../comments/<comment_id> endpoint that takes
> GET, PATCH, and PUT requests. The tests cover retrieval and update
> requests and handle calls from the various API versions. Also, they
> handle permissions for update requests on the new `addressed` field and
> invalid update values for the `addressed` field.
>
> Add `addressed` field to create_patch_comment helper in api tests
> utils.py.
>
> Signed-off-by: Raxel Gutierrez <raxel at google.com>
> ---
> docs/api/schemas/generate-schemas.py | 4 +-
> docs/api/schemas/latest/patchwork.yaml | 93 +-
> docs/api/schemas/patchwork.j2 | 97 +
> docs/api/schemas/v1.3/patchwork.yaml | 2704 ++++++++++++++++++++++++
> patchwork/api/base.py | 24 +-
> patchwork/api/check.py | 20 +-
> patchwork/api/comment.py | 70 +-
> patchwork/tests/api/test_comment.py | 199 +-
> patchwork/urls.py | 15 +-
> 9 files changed, 3171 insertions(+), 55 deletions(-)
> create mode 100644 docs/api/schemas/v1.3/patchwork.yaml
>
> diff --git a/docs/api/schemas/generate-schemas.py b/docs/api/schemas/generate-schemas.py
> index a0c1e45..3a436a1 100755
> --- a/docs/api/schemas/generate-schemas.py
> +++ b/docs/api/schemas/generate-schemas.py
> @@ -14,8 +14,8 @@ except ImportError:
> yaml = None
>
> ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
> -VERSIONS = [(1, 0), (1, 1), (1, 2), None]
> -LATEST_VERSION = (1, 2)
> +VERSIONS = [(1, 0), (1, 1), (1, 2), (1, 3), None]
> +LATEST_VERSION = (1, 3)
>
>
> def generate_schemas():
> diff --git a/docs/api/schemas/latest/patchwork.yaml b/docs/api/schemas/latest/patchwork.yaml
> index a8910a7..0d56b93 100644
> --- a/docs/api/schemas/latest/patchwork.yaml
> +++ b/docs/api/schemas/latest/patchwork.yaml
> @@ -13,7 +13,7 @@ info:
> license:
> name: GPL v2 License
> url: https://www.gnu.org/licenses/gpl-2.0.html
> - version: '1.2'
> + version: '1.3'
> paths:
> /api/:
> get:
> @@ -635,6 +635,72 @@ paths:
> $ref: '#/components/schemas/Error'
> tags:
> - comments
> + /api/patches/{patch_id}/comments/{comment_id}/:
> + parameters:
> + - in: path
> + name: patch_id
> + description: A unique integer value identifying the parent patch.
> + required: true
> + schema:
> + title: Patch ID
> + type: integer
> + - in: path
> + name: comment_id
> + description: A unique integer value identifying this comment.
> + required: true
> + schema:
> + title: Comment ID
> + type: integer
> + get:
> + description: Show a patch comment.
> + operationId: patch_comments_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Comment'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> + patch:
> + description: Update a patch comment (partial).
> + operationId: patch_comments_partial_update
> + requestBody:
> + $ref: '#/components/requestBodies/Comment'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Comment'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorCommentUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> /api/patches/{patch_id}/checks/:
> parameters:
> - in: path
> @@ -1242,6 +1308,12 @@ components:
> application/x-www-form-urlencoded:
> schema:
> $ref: '#/components/schemas/CheckCreate'
> + Comment:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/CommentUpdate'
> Patch:
> required: true
> content:
> @@ -1528,6 +1600,15 @@ components:
> additionalProperties:
> type: string
> readOnly: true
> + addressed:
> + title: Addressed
> + type: boolean
> + CommentUpdate:
> + type: object
> + properties:
> + addressed:
> + title: Addressed
> + type: boolean
> CoverList:
> type: object
> properties:
> @@ -1712,9 +1793,11 @@ components:
> previous_relation:
> title: Previous relation
> type: string
> + nullable: true
> current_relation:
> title: Current relation
> type: string
> + nullable: true
> EventPatchDelegated:
> allOf:
> - $ref: '#/components/schemas/EventBase'
> @@ -2555,6 +2638,14 @@ components:
> items:
> type: string
> readOnly: true
> + ErrorCommentUpdate:
> + type: object
> + properties:
> + addressed:
> + title: Addressed
> + type: array
> + items:
> + type: string
> ErrorPatchUpdate:
> type: object
> properties:
> diff --git a/docs/api/schemas/patchwork.j2 b/docs/api/schemas/patchwork.j2
> index af20743..9c159e7 100644
> --- a/docs/api/schemas/patchwork.j2
> +++ b/docs/api/schemas/patchwork.j2
> @@ -656,6 +656,74 @@ paths:
> $ref: '#/components/schemas/Error'
> tags:
> - comments
> +{% if version >= (1, 3) %}
> + /api/{{ version_url }}patches/{patch_id}/comments/{comment_id}/:
> + parameters:
> + - in: path
> + name: patch_id
> + description: A unique integer value identifying the parent patch.
> + required: true
> + schema:
> + title: Patch ID
> + type: integer
> + - in: path
> + name: comment_id
> + description: A unique integer value identifying this comment.
> + required: true
> + schema:
> + title: Comment ID
> + type: integer
> + get:
> + description: Show a patch comment.
> + operationId: patch_comments_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Comment'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> + patch:
> + description: Update a patch comment (partial).
> + operationId: patch_comments_partial_update
> + requestBody:
> + $ref: '#/components/requestBodies/Comment'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Comment'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorCommentUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> +{% endif %}
> /api/{{ version_url }}patches/{patch_id}/checks/:
> parameters:
> - in: path
> @@ -1277,6 +1345,14 @@ components:
> application/x-www-form-urlencoded:
> schema:
> $ref: '#/components/schemas/CheckCreate'
> +{% if version >= (1, 3) %}
> + Comment:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/CommentUpdate'
> +{% endif %}
> Patch:
> required: true
> content:
> @@ -1586,6 +1662,17 @@ components:
> additionalProperties:
> type: string
> readOnly: true
> +{% if version >= (1, 3) %}
> + addressed:
> + title: Addressed
> + type: boolean
> + CommentUpdate:
> + type: object
> + properties:
> + addressed:
> + title: Addressed
> + type: boolean
> +{% endif %}
> CoverList:
> type: object
> properties:
> @@ -2659,6 +2746,16 @@ components:
> items:
> type: string
> readOnly: true
> +{% if version >= (1, 3) %}
> + ErrorCommentUpdate:
> + type: object
> + properties:
> + addressed:
> + title: Addressed
> + type: array
> + items:
> + type: string
> +{% endif %}
> ErrorPatchUpdate:
> type: object
> properties:
> diff --git a/docs/api/schemas/v1.3/patchwork.yaml b/docs/api/schemas/v1.3/patchwork.yaml
> new file mode 100644
> index 0000000..fdf131c
> --- /dev/null
> +++ b/docs/api/schemas/v1.3/patchwork.yaml
> @@ -0,0 +1,2704 @@
> +# DO NOT EDIT THIS FILE. It is generated from a template. Changes should be
> +# proposed against the template and updated files generated using the
> +# 'generate-schemas.py' tool
> +---
> +openapi: '3.0.0'
> +info:
> + title: Patchwork API
> + description: >
> + Patchwork is a web-based patch tracking system designed to facilitate the
> + contribution and management of contributions to an open-source project.
> + contact:
> + email: patchwork at lists.ozlabs.org
> + license:
> + name: GPL v2 License
> + url: https://www.gnu.org/licenses/gpl-2.0.html
> + version: '1.3'
> +paths:
> + /api/1.3/:
> + get:
> + description: List API resources.
> + operationId: api_list
> + parameters: []
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Index'
> + tags:
> + - api
> + /api/1.3/bundles/:
> + get:
> + description: List bundles.
> + operationId: bundles_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + - in: query
> + name: project
> + description: An ID or linkname of a project to filter bundles by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: owner
> + description: An ID or username of a user to filter bundles by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: public
> + description: Show only public (`true`) or private (`false`) bundles.
> + schema:
> + title: ''
> + type: string
> + enum:
> + - 'true'
> + - 'false'
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Bundle'
> + tags:
> + - bundles
> + post:
> + description: Create a bundle.
> + operationId: bundles_create
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Bundle'
> + responses:
> + '201':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Bundle'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorBundleCreateUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - bundles
> + /api/1.3/bundles/{id}/:
> + parameters:
> + - in: path
> + name: id
> + required: true
> + description: A unique integer value identifying this bundle.
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: Show a bundle.
> + operationId: bundles_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Bundle'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - bundles
> + patch:
> + description: Update a bundle (partial).
> + operationId: bundles_partial_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Bundle'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Bundle'
> + '400':
> + description: Bad request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorBundleCreateUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - bundles
> + put:
> + description: Update a bundle.
> + operationId: bundles_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Bundle'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Bundle'
> + '400':
> + description: Bad request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorBundleCreateUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - bundles
> + /api/1.3/covers/:
> + get:
> + description: List cover letters.
> + operationId: covers_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + - $ref: '#/components/parameters/BeforeFilter'
> + - $ref: '#/components/parameters/SinceFilter'
> + - in: query
> + name: project
> + description: >
> + An ID or linkname of a project to filter cover letters by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: series
> + description: An ID of a series to filter cover letters by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: submitter
> + description: >
> + An ID or email address of a person to filter cover letters by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: msgid
> + description: >
> + The cover message-id as a case-sensitive string, without leading or
> + trailing angle brackets, to filter by.
> + schema:
> + title: ''
> + type: string
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/CoverList'
> + tags:
> + - covers
> + /api/1.3/covers/{id}/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying this cover letter.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: Show a cover letter.
> + operationId: covers_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/CoverDetail'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - covers
> + /api/1.3/covers/{id}/comments/:
> + parameters:
> + - in: path
> + name: id
> + description: >
> + A unique integer value identifying the parent cover letter.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: List comments
> + operationId: cover_comments_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Comment'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> + /api/1.3/events/:
> + get:
> + description: List events.
> + operationId: events_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + - $ref: '#/components/parameters/BeforeFilter'
> + - $ref: '#/components/parameters/SinceFilter'
> + - in: query
> + name: project
> + description: An ID or linkname of a project to filter events by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: category
> + description: An event category to filter events by.
> + schema:
> + title: ''
> + type: string
> + enum:
> + - cover-created
> + - patch-created
> + - patch-completed
> + - patch-state-changed
> + - patch-relation-changed
> + - patch-delegated
> + - check-created
> + - series-created
> + - series-completed
> + - in: query
> + name: series
> + description: An ID of a series to filter events by.
> + schema:
> + title: ''
> + type: integer
> + - in: query
> + name: patch
> + description: An ID of a patch to filter events by.
> + schema:
> + title: ''
> + type: integer
> + - in: query
> + name: cover
> + description: An ID of a cover letter to filter events by.
> + schema:
> + title: ''
> + type: integer
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + anyOf:
> + - $ref: '#/components/schemas/EventCoverCreated'
> + - $ref: '#/components/schemas/EventPatchCreated'
> + - $ref: '#/components/schemas/EventPatchCompleted'
> + - $ref: '#/components/schemas/EventPatchStateChanged'
> + - $ref: '#/components/schemas/EventPatchRelationChanged'
> + - $ref: '#/components/schemas/EventPatchDelegated'
> + - $ref: '#/components/schemas/EventCheckCreated'
> + - $ref: '#/components/schemas/EventSeriesCreated'
> + - $ref: '#/components/schemas/EventSeriesCompleted'
> + discriminator:
> + propertyName: category
> + mapping:
> + cover-created: '#/components/schemas/EventCoverCreated'
> + patch-created: '#/components/schemas/EventPatchCreated'
> + patch-completed: >
> + '#/components/schemas/EventPatchCompleted'
> + patch-state-changed: >
> + '#/components/schemas/EventPatchStateChanged'
> + patch-relation-changed: >
> + '#/components/schemas/EventPatchRelationChanged'
> + patch-delegated: >
> + '#/components/schemas/EventPatchDelegated'
> + check-created: '#/components/schemas/EventCheckCreated'
> + series-created: '#/components/schemas/EventSeriesCreated'
> + series-completed: >
> + '#/components/schemas/EventSeriesCompleted'
> + tags:
> + - events
> + /api/1.3/patches/:
> + get:
> + description: List patches.
> + operationId: patches_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + - $ref: '#/components/parameters/BeforeFilter'
> + - $ref: '#/components/parameters/SinceFilter'
> + - in: query
> + name: project
> + description: An ID or linkname of a project to filter patches by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: series
> + description: An ID of a series to filter patches by.
> + schema:
> + title: ''
> + type: integer
> + - in: query
> + name: submitter
> + description: >
> + An ID or email address of a person to filter patches by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: delegate
> + description: An ID or username of a user to filter patches by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: state
> + description: A slug representation of a state to filter patches by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: archived
> + description: >
> + Show only archived (`true`) or non-archived (`false`) patches.
> + schema:
> + title: ''
> + type: string
> + enum:
> + - 'true'
> + - 'false'
> + - in: query
> + name: hash
> + description: >
> + The patch hash as a case-insensitive hexadecimal string, to filter by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: msgid
> + description: >
> + The patch message-id as a case-sensitive string, without leading or
> + trailing angle brackets, to filter by.
> + schema:
> + title: ''
> + type: string
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/PatchList'
> + tags:
> + - patches
> + /api/1.3/patches/{id}/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying this patch.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: Show a patch.
> + operationId: patches_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/PatchDetail'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - patches
> + patch:
> + description: Update a patch (partial).
> + operationId: patches_partial_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Patch'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/PatchDetail'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorPatchUpdate'
> + '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:
> + - patches
> + put:
> + description: Update a patch.
> + operationId: patches_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Patch'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/PatchDetail'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorPatchUpdate'
> + '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:
> + - patches
> + /api/1.3/patches/{id}/comments/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying the parent patch.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: List comments
> + operationId: patch_comments_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Comment'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> + /api/1.3/patches/{patch_id}/comments/{comment_id}/:
> + parameters:
> + - in: path
> + name: patch_id
> + description: A unique integer value identifying the parent patch.
> + required: true
> + schema:
> + title: Patch ID
> + type: integer
> + - in: path
> + name: comment_id
> + description: A unique integer value identifying this comment.
> + required: true
> + schema:
> + title: Comment ID
> + type: integer
> + get:
> + description: Show a patch comment.
> + operationId: patch_comments_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Comment'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> + patch:
> + description: Update a patch comment (partial).
> + operationId: patch_comments_partial_update
> + requestBody:
> + $ref: '#/components/requestBodies/Comment'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Comment'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorCommentUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - comments
> + /api/1.3/patches/{patch_id}/checks/:
> + parameters:
> + - in: path
> + name: patch_id
> + description: A unique integer value identifying the parent patch.
> + required: true
> + schema:
> + title: Patch ID
> + type: integer
> + get:
> + description: List checks.
> + operationId: checks_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + - $ref: '#/components/parameters/BeforeFilter'
> + - $ref: '#/components/parameters/SinceFilter'
> + - in: query
> + name: user
> + description: An ID or username of a user to filter checks by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: state
> + description: A check state to filter checks by.
> + schema:
> + title: ''
> + type: string
> + enum:
> + - pending
> + - success
> + - warning
> + - fail
> + - in: query
> + name: context
> + description: A check context to filter checks by.
> + schema:
> + title: ''
> + type: string
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Check'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - checks
> + post:
> + description: Create a check.
> + operationId: checks_create
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Check'
> + responses:
> + '201':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Check'
> + '400':
> + description: Invalid Request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorCheckCreate'
> + '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.3/patches/{patch_id}/checks/{check_id}/:
> + parameters:
> + - in: path
> + name: patch_id
> + description: A unique integer value identifying the parent patch.
> + required: true
> + schema:
> + title: Patch ID
> + type: integer
> + - in: path
> + name: check_id
> + description: A unique integer value identifying this check.
> + required: true
> + schema:
> + title: Check ID
> + type: integer
> + get:
> + description: Show a check.
> + operationId: checks_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Check'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - checks
> + /api/1.3/people/:
> + get:
> + description: List people.
> + operationId: people_list
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Person'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - people
> + /api/1.3/people/{id}/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying this person.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: Show a person.
> + operationId: people_read
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Person'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - people
> + /api/1.3/projects/:
> + get:
> + description: List projects.
> + operationId: projects_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Project'
> + tags:
> + - projects
> + /api/1.3/projects/{id}/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying this project.
> + required: true
> + schema:
> + title: ID
> + # TODO: Add regex?
> + type: string
> + get:
> + description: Show a project.
> + operationId: projects_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Project'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - projects
> + patch:
> + description: Update a project (partial).
> + operationId: projects_partial_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Project'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Project'
> + '400':
> + description: Bad request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorProjectUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - projects
> + put:
> + description: Update a project.
> + operationId: projects_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/Project'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Project'
> + '400':
> + description: Bad request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorProjectUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - projects
> + /api/1.3/series/:
> + get:
> + description: List series.
> + operationId: series_list
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + - $ref: '#/components/parameters/BeforeFilter'
> + - $ref: '#/components/parameters/SinceFilter'
> + - in: query
> + name: submitter
> + description: An ID or email address of a person to filter series by.
> + schema:
> + title: ''
> + type: string
> + - in: query
> + name: project
> + description: An ID or linkname of a project to filter series by.
> + schema:
> + title: ''
> + type: string
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/Series'
> + tags:
> + - series
> + /api/1.3/series/{id}/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying this series.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: Show a series.
> + operationId: series_read
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Series'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - series
> + /api/1.3/users/:
> + get:
> + description: List users.
> + operationId: users_list
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + parameters:
> + - $ref: '#/components/parameters/Page'
> + - $ref: '#/components/parameters/PageSize'
> + - $ref: '#/components/parameters/Order'
> + - $ref: '#/components/parameters/Search'
> + responses:
> + '200':
> + description: ''
> + headers:
> + Link:
> + $ref: '#/components/headers/Link'
> + content:
> + application/json:
> + schema:
> + type: array
> + items:
> + $ref: '#/components/schemas/User'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - users
> + /api/1.3/users/{id}/:
> + parameters:
> + - in: path
> + name: id
> + description: A unique integer value identifying this user.
> + required: true
> + schema:
> + title: ID
> + type: integer
> + get:
> + description: Show a user.
> + operationId: users_read
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/UserDetail'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - users
> + patch:
> + description: Update a user (partial).
> + operationId: users_partial_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/User'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/UserDetail'
> + '400':
> + description: Bad request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorUserUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - users
> + put:
> + description: Update a user.
> + operationId: users_update
> +# security:
> +# - basicAuth: []
> +# - apiKeyAuth: []
> + requestBody:
> + $ref: '#/components/requestBodies/User'
> + responses:
> + '200':
> + description: ''
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/UserDetail'
> + '400':
> + description: Bad request
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/ErrorUserUpdate'
> + '403':
> + description: Forbidden
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + '404':
> + description: Not found
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Error'
> + tags:
> + - users
> +components:
> + securitySchemes:
> + basicAuth:
> + type: http
> + scheme: basic
> + apiKeyAuth:
> + type: http
> + scheme: bearer
> + parameters:
> + Page:
> + in: query
> + name: page
> + description: A page number within the paginated result set.
> + schema:
> + title: Page
> + type: integer
> + PageSize:
> + in: query
> + name: per_page
> + description: Number of results to return per page.
> + schema:
> + title: Page size
> + type: integer
> + Order:
> + in: query
> + name: order
> + description: Which field to use when ordering the results.
> + schema:
> + title: Ordering
> + type: string
> + Search:
> + in: query
> + name: q
> + description: A search term.
> + schema:
> + title: Search
> + type: string
> + BeforeFilter:
> + in: query
> + name: before
> + description: Latest date-time to retrieve results for.
> + schema:
> + title: ''
> + type: string
> + SinceFilter:
> + in: query
> + name: since
> + description: Earliest date-time to retrieve results for.
> + schema:
> + title: ''
> + type: string
> + headers:
> + Link:
> + description: >
> + Links to related resources, in the format defined by
> + [RFC 5988](https://tools.ietf.org/html/rfc5988#section-5).
> + This will include a link with relation type `next` to the
> + next page, if there is a next page.
> + schema:
> + type: string
> + requestBodies:
> + Bundle:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/BundleCreateUpdate'
> + multipart/form-data:
> + schema:
> + $ref: '#/components/schemas/BundleCreateUpdate'
> + application/x-www-form-urlencoded:
> + schema:
> + $ref: '#/components/schemas/BundleCreateUpdate'
> + Check:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/CheckCreate'
> + multipart/form-data:
> + schema:
> + $ref: '#/components/schemas/CheckCreate'
> + application/x-www-form-urlencoded:
> + schema:
> + $ref: '#/components/schemas/CheckCreate'
> + Comment:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/CommentUpdate'
> + Patch:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/PatchUpdate'
> + multipart/form-data:
> + schema:
> + $ref: '#/components/schemas/PatchUpdate'
> + application/x-www-form-urlencoded:
> + schema:
> + $ref: '#/components/schemas/PatchUpdate'
> + Project:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/Project'
> + multipart/form-data:
> + schema:
> + $ref: '#/components/schemas/Project'
> + application/x-www-form-urlencoded:
> + schema:
> + $ref: '#/components/schemas/Project'
> + User:
> + required: true
> + content:
> + application/json:
> + schema:
> + $ref: '#/components/schemas/UserDetail'
> + multipart/form-data:
> + schema:
> + $ref: '#/components/schemas/UserDetail'
> + application/x-www-form-urlencoded:
> + schema:
> + $ref: '#/components/schemas/UserDetail'
> + schemas:
> + Index:
> + type: object
> + properties:
> + bundles:
> + title: Bundles URL
> + type: string
> + format: uri
> + readOnly: true
> + covers:
> + title: Covers URL
> + type: string
> + format: uri
> + readOnly: true
> + events:
> + title: Events URL
> + type: string
> + format: uri
> + readOnly: true
> + patches:
> + title: Patches URL
> + type: string
> + format: uri
> + readOnly: true
> + people:
> + title: People URL
> + type: string
> + format: uri
> + readOnly: true
> + projects:
> + title: Projects URL
> + type: string
> + format: uri
> + readOnly: true
> + users:
> + title: Users URL
> + type: string
> + format: uri
> + readOnly: true
> + series:
> + title: Series URL
> + type: string
> + format: uri
> + readOnly: true
> + Bundle:
> + required:
> + - name
> + 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
> + project:
> + $ref: '#/components/schemas/ProjectEmbedded'
> + name:
> + title: Name
> + type: string
> + minLength: 1
> + maxLength: 50
> + owner:
> + type: object
> + title: Owner
> + readOnly: true
> + nullable: false
> + allOf:
> + - $ref: '#/components/schemas/UserEmbedded'
> + patches:
> + title: Patches
> + type: array
> + items:
> + $ref: '#/components/schemas/PatchEmbedded'
> + uniqueItems: true
> + public:
> + title: Public
> + type: boolean
> + mbox:
> + title: Mbox
> + type: string
> + format: uri
> + readOnly: true
> + BundleCreateUpdate:
> + type: object
> + required:
> + - name
> + properties:
> + name:
> + title: Name
> + type: string
> + minLength: 1
> + maxLength: 50
> + patches:
> + title: Patches
> + type: array
> + items:
> + type: integer
> + uniqueItems: true
> + public:
> + title: Public
> + type: boolean
> + Check:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: Url
> + type: string
> + format: uri
> + readOnly: true
> + user:
> + $ref: '#/components/schemas/UserEmbedded'
> + date:
> + title: Date
> + type: string
> + format: iso8601
> + readOnly: true
> + state:
> + title: State
> + description: The state of the check.
> + type: string
> + enum:
> + - pending
> + - success
> + - warning
> + - fail
> + target_url:
> + title: Target URL
> + description: >
> + The target URL to associate with this check. This should be
> + specific to the patch.
> + type: string
> + format: uri
> + maxLength: 200
> + nullable: true
> + context:
> + title: Context
> + description: >
> + A label to discern check from checks of other testing systems.
> + type: string
> + pattern: ^[-a-zA-Z0-9_]+$
> + minLength: 1
> + maxLength: 255
> + description:
> + title: Description
> + description: A brief description of the check.
> + type: string
> + nullable: true
> + CheckCreate:
> + type: object
> + required:
> + - state
> + properties:
> + state:
> + title: State
> + description: The state of the check.
> + type: string
> + enum:
> + - pending
> + - success
> + - warning
> + - fail
> + target_url:
> + title: Target URL
> + description:
> + The target URL to associate with this check. This should be
> + specific to the patch.
> + type: string
> + format: uri
> + maxLength: 200
> + nullable: true
> + context:
> + title: Context
> + description: >
> + A label to discern check from checks of other testing systems.
> + type: string
> + pattern: ^[-a-zA-Z0-9_]+$
> + minLength: 1
> + maxLength: 255
> + description:
> + title: Description
> + description: A brief description of the check.
> + type: string
> + nullable: true
> + Comment:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + web_url:
> + title: Web URL
> + type: string
> + format: uri
> + readOnly: true
> + msgid:
> + title: Message ID
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + list_archive_url:
> + title: List archive URL
> + type: string
> + readOnly: true
> + nullable: true
> + date:
> + title: Date
> + type: string
> + format: iso8601
> + readOnly: true
> + subject:
> + title: Subject
> + type: string
> + readOnly: true
> + submitter:
> + type: object
> + title: Submitter
> + allOf:
> + - $ref: '#/components/schemas/PersonEmbedded'
> + content:
> + title: Content
> + type: string
> + readOnly: true
> + minLength: 1
> + headers:
> + title: Headers
> + anyOf:
> + - type: object
> + additionalProperties:
> + type: array
> + items:
> + type: string
> + - type: object
> + additionalProperties:
> + type: string
> + readOnly: true
> + addressed:
> + title: Addressed
> + type: boolean
> + CommentUpdate:
> + type: object
> + properties:
> + addressed:
> + title: Addressed
> + type: boolean
> + CoverList:
> + 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
> + project:
> + $ref: '#/components/schemas/ProjectEmbedded'
> + msgid:
> + title: Message ID
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + 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
> + maxLength: 255
> + submitter:
> + type: object
> + title: Submitter
> + readOnly: true
> + allOf:
> + - $ref: '#/components/schemas/PersonEmbedded'
> + mbox:
> + title: Mbox
> + type: string
> + format: uri
> + readOnly: true
> + series:
> + type: array
> + items:
> + $ref: '#/components/schemas/SeriesEmbedded'
> + readOnly: true
> + comments:
> + title: Comments
> + type: string
> + format: uri
> + readOnly: true
> + CoverDetail:
> + allOf:
> + - $ref: '#/components/schemas/CoverList'
> + - properties:
> + headers:
> + title: Headers
> + anyOf:
> + - type: object
> + additionalProperties:
> + type: array
> + items:
> + type: string
> + - type: object
> + additionalProperties:
> + type: string
> + readOnly: true
> + content:
> + title: Content
> + type: string
> + readOnly: true
> + minLength: 1
> + EventBase:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + category:
> + title: Category
> + description: The category of the event.
> + type: string
> + readOnly: true
> + project:
> + $ref: '#/components/schemas/ProjectEmbedded'
> + date:
> + title: Date
> + description: The time this event was created.
> + type: string
> + format: iso8601
> + readOnly: true
> + actor:
> + type: object
> + title: Actor
> + description: The user that caused/created this event.
> + readOnly: true
> + nullable: true
> + allOf:
> + - $ref: '#/components/schemas/UserEmbedded'
> + payload:
> + type: object
> + EventCoverCreated:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - cover-created
> + payload:
> + properties:
> + cover:
> + $ref: '#/components/schemas/CoverEmbedded'
> + EventPatchCreated:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - patch-created
> + payload:
> + properties:
> + patch:
> + $ref: '#/components/schemas/PatchEmbedded'
> + EventPatchCompleted:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - patch-completed
> + payload:
> + properties:
> + patch:
> + $ref: '#/components/schemas/PatchEmbedded'
> + series:
> + $ref: '#/components/schemas/SeriesEmbedded'
> + EventPatchStateChanged:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - patch-state-changed
> + payload:
> + properties:
> + patch:
> + $ref: '#/components/schemas/PatchEmbedded'
> + previous_state:
> + title: Previous state
> + type: string
> + current_state:
> + title: Current state
> + type: string
> + EventPatchRelationChanged:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - patch-relation-changed
> + payload:
> + properties:
> + patch:
> + $ref: '#/components/schemas/PatchEmbedded'
> + previous_relation:
> + title: Previous relation
> + type: string
> + nullable: true
> + current_relation:
> + title: Current relation
> + type: string
> + nullable: true
> + EventPatchDelegated:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - patch-delegated
> + payload:
> + properties:
> + patch:
> + $ref: '#/components/schemas/PatchEmbedded'
> + previous_delegate:
> + $ref: '#/components/schemas/UserEmbedded'
> + current_delegate:
> + $ref: '#/components/schemas/UserEmbedded'
> + EventCheckCreated:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - check-created
> + payload:
> + properties:
> + patch:
> + $ref: '#/components/schemas/PatchEmbedded'
> + check:
> + $ref: '#/components/schemas/CheckEmbedded'
> + EventSeriesCreated:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - series-created
> + payload:
> + properties:
> + series:
> + $ref: '#/components/schemas/SeriesEmbedded'
> + EventSeriesCompleted:
> + allOf:
> + - $ref: '#/components/schemas/EventBase'
> + - type: object
> + properties:
> + category:
> + enum:
> + - series-completed
> + payload:
> + properties:
> + series:
> + $ref: '#/components/schemas/SeriesEmbedded'
> + PatchList:
> + required:
> + - state
> + - delegate
> + 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
> + project:
> + $ref: '#/components/schemas/ProjectEmbedded'
> + msgid:
> + title: Message ID
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + 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
> + maxLength: 255
> + commit_ref:
> + title: Commit ref
> + type: string
> + maxLength: 255
> + nullable: true
> + pull_url:
> + title: Pull URL
> + type: string
> + format: uri
> + maxLength: 255
> + nullable: true
> + state:
> + title: State
> + type: string
> + archived:
> + title: Archived
> + type: boolean
> + hash:
> + title: Hash
> + type: string
> + readOnly: true
> + minLength: 1
> + submitter:
> + type: object
> + title: Submitter
> + readOnly: true
> + allOf:
> + - $ref: '#/components/schemas/PersonEmbedded'
> + delegate:
> + type: object
> + title: Delegate
> + nullable: true
> + readOnly: true
> + allOf:
> + - $ref: '#/components/schemas/UserEmbedded'
> + mbox:
> + title: Mbox
> + type: string
> + format: uri
> + readOnly: true
> + series:
> + type: array
> + items:
> + $ref: '#/components/schemas/SeriesEmbedded'
> + readOnly: true
> + comments:
> + title: Comments
> + type: string
> + format: uri
> + readOnly: true
> + check:
> + title: Check
> + type: string
> + readOnly: true
> + enum:
> + - pending
> + - success
> + - warning
> + - fail
> + checks:
> + title: Checks
> + type: string
> + format: uri
> + readOnly: true
> + tags:
> + title: Tags
> + type: object
> + additionalProperties:
> + type: string
> + readOnly: true
> + related:
> + title: Relations
> + type: array
> + items:
> + $ref: '#/components/schemas/PatchEmbedded'
> + PatchDetail:
> + allOf:
> + - $ref: '#/components/schemas/PatchList'
> + - properties:
> + headers:
> + title: Headers
> + anyOf:
> + - type: object
> + additionalProperties:
> + type: array
> + items:
> + type: string
> + - type: object
> + additionalProperties:
> + type: string
> + readOnly: true
> + content:
> + title: Content
> + type: string
> + readOnly: true
> + minLength: 1
> + diff:
> + title: Diff
> + type: string
> + readOnly: true
> + minLength: 1
> + prefixes:
> + title: Prefixes
> + type: array
> + items:
> + type: string
> + readOnly: true
> + PatchUpdate:
> + type: object
> + properties:
> + commit_ref:
> + title: Commit ref
> + type: string
> + maxLength: 255
> + nullable: true
> + pull_url:
> + title: Pull URL
> + type: string
> + format: uri
> + maxLength: 255
> + nullable: true
> + state:
> + title: State
> + type: string
> + archived:
> + title: Archived
> + type: boolean
> + delegate:
> + title: Delegate
> + type: integer
> + nullable: true
> + related:
> + title: Relations
> + type: array
> + items:
> + type: integer
> + Person:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: URL
> + type: string
> + format: uri
> + readOnly: true
> + name:
> + title: Name
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + email:
> + title: Email
> + type: string
> + format: email
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + user:
> + type: object
> + title: User
> + nullable: true
> + readOnly: true
> + allOf:
> + - $ref: '#/components/schemas/UserEmbedded'
> + Project:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: URL
> + type: string
> + format: uri
> + readOnly: true
> + name:
> + title: Name
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + link_name:
> + title: Link name
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + list_id:
> + title: List ID
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 255
> + list_email:
> + title: List email
> + type: string
> + format: email
> + readOnly: true
> + minLength: 1
> + maxLength: 200
> + web_url:
> + title: Web URL
> + type: string
> + format: uri
> + maxLength: 2000
> + scm_url:
> + title: SCM URL
> + type: string
> + format: uri
> + maxLength: 2000
> + webscm_url:
> + title: Web SCM URL
> + type: string
> + format: uri
> + maxLength: 2000
> + maintainers:
> + type: array
> + items:
> + $ref: '#/components/schemas/UserEmbedded'
> + readOnly: true
> + uniqueItems: true
> + subject_match:
> + title: Subject match
> + description: >
> + Regex to match the subject against if only part of emails sent to
> + the list belongs to this project. Will be used with IGNORECASE and
> + MULTILINE flags. If rules for more projects match the first one
> + returned from DB is chosen; empty field serves as a default for
> + every email which has no other match.
> + type: string
> + readOnly: true
> + maxLength: 64
> + list_archive_url:
> + title: List archive URL
> + type: string
> + format: uri
> + maxLength: 2000
> + nullable: true
> + list_archive_url_format:
> + title: List archive URL format
> + type: string
> + format: uri
> + maxLength: 2000
> + nullable: true
> + description: >
> + URL format for the list archive's Message-ID redirector. {} will be
> + replaced by the Message-ID.
> + commit_url_format:
> + title: Web SCM URL format for a particular commit
> + type: string
> + Series:
> + 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
> + project:
> + $ref: '#/components/schemas/ProjectEmbedded'
> + name:
> + title: Name
> + description: >
> + An optional name to associate with the series, e.g. "John's PCI
> + series".
> + type: string
> + maxLength: 255
> + nullable: true
> + date:
> + title: Date
> + type: string
> + format: iso8601
> + readOnly: true
> + submitter:
> + type: object
> + title: Submitter
> + readOnly: true
> + allOf:
> + - $ref: '#/components/schemas/PersonEmbedded'
> + version:
> + title: Version
> + description: >
> + Version of series as indicated by the subject prefix(es).
> + type: integer
> + total:
> + title: Total
> + description: >
> + Number of patches in series as indicated by the subject prefix(es).
> + type: integer
> + readOnly: true
> + received_total:
> + title: Received total
> + type: integer
> + readOnly: true
> + received_all:
> + title: Received all
> + type: boolean
> + readOnly: true
> + mbox:
> + title: Mbox
> + type: string
> + format: uri
> + readOnly: true
> + cover_letter:
> + $ref: '#/components/schemas/CoverEmbedded'
> + patches:
> + title: Patches
> + type: array
> + items:
> + $ref: '#/components/schemas/PatchEmbedded'
> + readOnly: true
> + uniqueItems: true
> + User:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: URL
> + type: string
> + format: uri
> + readOnly: true
> + username:
> + title: Username
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 150
> + first_name:
> + title: First name
> + type: string
> + maxLength: 30
> + last_name:
> + title: Last name
> + type: string
> + maxLength: 150
> + email:
> + title: Email address
> + type: string
> + format: email
> + readOnly: true
> + minLength: 1
> + UserDetail:
> + type: object
> + allOf:
> + - $ref: '#/components/schemas/User'
> + - type: object
> + properties:
> + settings:
> + type: object
> + properties:
> + send_email:
> + title: Send email
> + description: >
> + Whether Patchwork should send email on your behalf.
> + Only present and configurable for your account.
> + type: boolean
> + items_per_page:
> + title: Items per page
> + description: >
> + Number of items to display per page (web UI).
> + Only present and configurable for your account.
> + type: integer
> + show_ids:
> + title: Show IDs
> + description:
> + Show click-to-copy IDs in the list view (web UI).
> + Only present and configurable for your account.
> + type: boolean
> + CheckEmbedded:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: Url
> + type: string
> + format: uri
> + readOnly: true
> + date:
> + title: Date
> + type: string
> + format: iso8601
> + readOnly: true
> + state:
> + title: State
> + description: The state of the check.
> + type: string
> + readOnly: true
> + enum:
> + - pending
> + - success
> + - warning
> + - fail
> + target_url:
> + title: Target url
> + description: >
> + The target URL to associate with this check. This should be specific
> + to the patch.
> + type: string
> + format: uri
> + maxLength: 200
> + nullable: true
> + readOnly: true
> + context:
> + title: Context
> + description: >
> + A label to discern check from checks of other testing systems.
> + type: string
> + pattern: ^[-a-zA-Z0-9_]+$
> + maxLength: 255
> + minLength: 1
> + readOnly: true
> + CoverEmbedded:
> + 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
> + PatchEmbedded:
> + 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
> + PersonEmbedded:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: URL
> + type: string
> + format: uri
> + readOnly: true
> + name:
> + title: Name
> + type: string
> + readOnly: true
> + minLength: 1
> + email:
> + title: Email
> + type: string
> + format: email
> + readOnly: true
> + minLength: 1
> + ProjectEmbedded:
> + type: object
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: URL
> + type: string
> + format: uri
> + readOnly: true
> + name:
> + title: Name
> + type: string
> + readOnly: true
> + minLength: 1
> + link_name:
> + title: Link name
> + type: string
> + readOnly: true
> + maxLength: 255
> + minLength: 1
> + list_id:
> + title: List ID
> + type: string
> + readOnly: true
> + maxLength: 255
> + minLength: 1
> + list_email:
> + title: List email
> + type: string
> + format: email
> + readOnly: true
> + maxLength: 200
> + minLength: 1
> + web_url:
> + title: Web URL
> + type: string
> + format: uri
> + readOnly: true
> + maxLength: 2000
> + scm_url:
> + title: SCM URL
> + type: string
> + format: uri
> + readOnly: true
> + maxLength: 2000
> + webscm_url:
> + title: WebSCM URL
> + type: string
> + format: uri
> + readOnly: true
> + maxLength: 2000
> + list_archive_url:
> + title: List archive URL
> + type: string
> + format: uri
> + maxLength: 2000
> + nullable: true
> + list_archive_url_format:
> + title: List archive URL format
> + type: string
> + format: uri
> + maxLength: 2000
> + nullable: true
> + description: >
> + URL format for the list archive's Message-ID redirector. {} will be
> + replaced by the Message-ID.
> + commit_url_format:
> + title: Web SCM URL format for a particular commit
> + type: string
> + readOnly: true
> + SeriesEmbedded:
> + 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
> + name:
> + title: Name
> + description: >
> + An optional name to associate with the series, e.g. "John's PCI
> + series".
> + type: string
> + readOnly: true
> + maxLength: 255
> + nullable: true
> + date:
> + title: Date
> + type: string
> + format: iso8601
> + readOnly: true
> + version:
> + title: Version
> + description: >
> + Version of series as indicated by the subject prefix(es).
> + type: integer
> + readOnly: true
> + mbox:
> + title: Mbox
> + type: string
> + format: uri
> + readOnly: true
> + UserEmbedded:
> + type: object
> + nullable: true
> + properties:
> + id:
> + title: ID
> + type: integer
> + readOnly: true
> + url:
> + title: URL
> + type: string
> + format: uri
> + readOnly: true
> + username:
> + title: Username
> + type: string
> + readOnly: true
> + minLength: 1
> + maxLength: 150
> + first_name:
> + title: First name
> + type: string
> + maxLength: 30
> + readOnly: true
> + last_name:
> + title: Last name
> + type: string
> + maxLength: 150
> + readOnly: true
> + email:
> + title: Email address
> + type: string
> + format: email
> + readOnly: true
> + minLength: 1
> + Error:
> + type: object
> + properties:
> + detail:
> + title: Detail
> + type: string
> + readOnly: true
> + ErrorBundleCreateUpdate:
> + type: object
> + properties:
> + name:
> + title: Name
> + type: array
> + items:
> + type: string
> + readOnly: true
> + patches:
> + title: Patches
> + type: array
> + items:
> + type: string
> + readOnly: true
> + public:
> + title: Public
> + type: array
> + items:
> + type: string
> + ErrorCheckCreate:
> + type: object
> + properties:
> + state:
> + title: State
> + type: array
> + items:
> + type: string
> + readOnly: true
> + target_url:
> + title: Target URL
> + type: array
> + items:
> + type: string
> + readOnly: true
> + context:
> + title: Context
> + type: array
> + items:
> + type: string
> + readOnly: true
> + description:
> + title: Description
> + type: array
> + items:
> + type: string
> + readOnly: true
> + ErrorCommentUpdate:
> + type: object
> + properties:
> + addressed:
> + title: Addressed
> + type: array
> + items:
> + type: string
> + ErrorPatchUpdate:
> + type: object
> + properties:
> + state:
> + title: State
> + type: array
> + items:
> + type: string
> + readOnly: true
> + delegate:
> + title: Delegate
> + type: array
> + items:
> + type: string
> + readOnly: true
> + commit_ref:
> + title: Commit ref
> + type: array
> + items:
> + type: string
> + readOnly: true
> + archived:
> + title: Archived
> + type: array
> + items:
> + type: string
> + readOnly: true
> + ErrorProjectUpdate:
> + type: object
> + properties:
> + web_url:
> + title: Web URL
> + type: string
> + format: uri
> + readOnly: true
> + scm_url:
> + title: SCM URL
> + type: string
> + format: uri
> + readOnly: true
> + webscm_url:
> + title: Web SCM URL
> + type: string
> + format: uri
> + readOnly: true
> + ErrorUserUpdate:
> + type: object
> + properties:
> + first_name:
> + title: First name
> + type: string
> + readOnly: true
> + last_name:
> + title: First name
> + type: string
> + readOnly: true
> diff --git a/patchwork/api/base.py b/patchwork/api/base.py
> index 89a4311..856fbd3 100644
> --- a/patchwork/api/base.py
> +++ b/patchwork/api/base.py
> @@ -3,6 +3,7 @@
> #
> # SPDX-License-Identifier: GPL-2.0-or-later
>
> +import rest_framework
>
> from django.conf import settings
> from django.shortcuts import get_object_or_404
> @@ -15,6 +16,24 @@ from rest_framework.serializers import HyperlinkedModelSerializer
> from patchwork.api import utils
>
>
> +DRF_VERSION = tuple(int(x) for x in rest_framework.__version__.split('.'))
> +
> +
> +if DRF_VERSION > (3, 11):
> + class CurrentPatchDefault(object):
> + requires_context = True
> +
> + def __call__(self, serializer_field):
> + return serializer_field.context['request'].patch
> +else:
> + class CurrentPatchDefault(object):
> + def set_context(self, serializer_field):
> + self.patch = serializer_field.context['request'].patch
> +
> + def __call__(self):
> + return self.patch
> +
> +
> class LinkHeaderPagination(PageNumberPagination):
> """Provide pagination based on rfc5988.
>
> @@ -44,7 +63,10 @@ class LinkHeaderPagination(PageNumberPagination):
>
>
> class PatchworkPermission(permissions.BasePermission):
> - """This permission works for Project and Patch model objects"""
> + """
> + This permission works for Project, Patch, and PatchComment
> + model objects
> + """
> def has_object_permission(self, request, view, obj):
> # read only for everyone
> if request.method in permissions.SAFE_METHODS:
> diff --git a/patchwork/api/check.py b/patchwork/api/check.py
> index a6bf5f8..2049d2f 100644
> --- a/patchwork/api/check.py
> +++ b/patchwork/api/check.py
> @@ -6,7 +6,6 @@
> from django.http import Http404
> from django.http.request import QueryDict
> from django.shortcuts import get_object_or_404
> -import rest_framework
> from rest_framework.exceptions import PermissionDenied
> from rest_framework.generics import ListCreateAPIView
> from rest_framework.generics import RetrieveAPIView
> @@ -17,30 +16,13 @@ from rest_framework.serializers import ValidationError
>
> from patchwork.api.base import CheckHyperlinkedIdentityField
> from patchwork.api.base import MultipleFieldLookupMixin
> +from patchwork.api.base import CurrentPatchDefault
> from patchwork.api.embedded import UserSerializer
> from patchwork.api.filters import CheckFilterSet
> from patchwork.models import Check
> from patchwork.models import Patch
>
>
> -DRF_VERSION = tuple(int(x) for x in rest_framework.__version__.split('.'))
> -
> -
> -if DRF_VERSION > (3, 11):
> - class CurrentPatchDefault(object):
> - requires_context = True
> -
> - def __call__(self, serializer_field):
> - return serializer_field.context['request'].patch
> -else:
> - class CurrentPatchDefault(object):
> - def set_context(self, serializer_field):
> - self.patch = serializer_field.context['request'].patch
> -
> - def __call__(self):
> - return self.patch
> -
> -
> class CheckSerializer(HyperlinkedModelSerializer):
>
> url = CheckHyperlinkedIdentityField('api-check-detail')
> diff --git a/patchwork/api/comment.py b/patchwork/api/comment.py
> index 0c578b4..ab54e22 100644
> --- a/patchwork/api/comment.py
> +++ b/patchwork/api/comment.py
> @@ -5,12 +5,17 @@
>
> import email.parser
>
> +from django.shortcuts import get_object_or_404
> from django.http import Http404
> from rest_framework.generics import ListAPIView
> +from rest_framework.generics import RetrieveUpdateAPIView
> +from rest_framework.serializers import HiddenField
> from rest_framework.serializers import SerializerMethodField
>
> from patchwork.api.base import BaseHyperlinkedModelSerializer
> +from patchwork.api.base import MultipleFieldLookupMixin
> from patchwork.api.base import PatchworkPermission
> +from patchwork.api.base import CurrentPatchDefault
> from patchwork.api.embedded import PersonSerializer
> from patchwork.models import Cover
> from patchwork.models import CoverComment
> @@ -66,15 +71,50 @@ class CoverCommentListSerializer(BaseCommentListSerializer):
> versioned_fields = BaseCommentListSerializer.Meta.versioned_fields
>
>
> -class PatchCommentListSerializer(BaseCommentListSerializer):
> +class PatchCommentSerializer(BaseCommentListSerializer):
> +
> + patch = HiddenField(default=CurrentPatchDefault())
>
> class Meta:
> model = PatchComment
> - fields = BaseCommentListSerializer.Meta.fields
> - read_only_fields = fields
> + fields = BaseCommentListSerializer.Meta.fields + (
> + 'patch', 'addressed')
> + read_only_fields = BaseCommentListSerializer.Meta.fields + ('patch', )
> + versioned_fields = {
> + '1.3': ('patch', 'addressed'),
> + }
> + extra_kwargs = {
> + 'url': {'view_name': 'api-patch-comment-detail'}
> + }
> versioned_fields = BaseCommentListSerializer.Meta.versioned_fields
>
>
> +class PatchCommentMixin(object):
> +
> + permission_classes = (PatchworkPermission,)
> + serializer_class = PatchCommentSerializer
> +
> + def get_object(self):
> + queryset = self.filter_queryset(self.get_queryset())
> + comment_id = self.kwargs['comment_id']
> + try:
> + obj = queryset.get(id=int(comment_id))
> + except (ValueError, PatchComment.DoesNotExist):
> + obj = get_object_or_404(queryset, linkname=comment_id)
> + self.kwargs['comment_id'] = obj.id
> + self.check_object_permissions(self.request, obj)
> + return obj
> +
> + def get_queryset(self):
> + patch_id = self.kwargs['patch_id']
> + if not Patch.objects.filter(id=patch_id).exists():
> + raise Http404
> +
> + return PatchComment.objects.filter(
> + patch=patch_id
> + ).select_related('submitter')
> +
> +
> class CoverCommentList(ListAPIView):
> """List cover comments"""
>
> @@ -94,20 +134,24 @@ class CoverCommentList(ListAPIView):
> ).select_related('submitter')
>
>
> -class PatchCommentList(ListAPIView):
> - """List comments"""
> +class PatchCommentList(PatchCommentMixin, ListAPIView):
> + """List patch comments"""
>
> - permission_classes = (PatchworkPermission,)
> - serializer_class = PatchCommentListSerializer
> search_fields = ('subject',)
> ordering_fields = ('id', 'subject', 'date', 'submitter')
> ordering = 'id'
> lookup_url_kwarg = 'patch_id'
>
> - def get_queryset(self):
> - if not Patch.objects.filter(id=self.kwargs['patch_id']).exists():
> - raise Http404
>
> - return PatchComment.objects.filter(
> - patch=self.kwargs['patch_id']
> - ).select_related('submitter')
> +class PatchCommentDetail(PatchCommentMixin, MultipleFieldLookupMixin,
> + RetrieveUpdateAPIView):
> + """
> + get:
> + Show a patch comment.
> + patch:
> + Update a patch comment.
> + put:
> + Update a patch comment.
> + """
> + lookup_url_kwargs = ('patch_id', 'comment_id')
> + lookup_fields = ('patch_id', 'id')
> diff --git a/patchwork/tests/api/test_comment.py b/patchwork/tests/api/test_comment.py
> index 59450d8..f43d1c7 100644
> --- a/patchwork/tests/api/test_comment.py
> +++ b/patchwork/tests/api/test_comment.py
> @@ -9,11 +9,16 @@ from django.conf import settings
> from django.urls import NoReverseMatch
> from django.urls import reverse
>
> +from patchwork.models import PatchComment
> from patchwork.tests.api import utils
> from patchwork.tests.utils import create_cover
> from patchwork.tests.utils import create_cover_comment
> from patchwork.tests.utils import create_patch
> from patchwork.tests.utils import create_patch_comment
> +from patchwork.tests.utils import create_maintainer
> +from patchwork.tests.utils import create_project
> +from patchwork.tests.utils import create_person
> +from patchwork.tests.utils import create_user
> from patchwork.tests.utils import SAMPLE_CONTENT
>
> if settings.ENABLE_REST_API:
> @@ -86,34 +91,40 @@ class TestCoverComments(utils.APITestCase):
> @unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
> class TestPatchComments(utils.APITestCase):
> @staticmethod
> - def api_url(patch, version=None):
> - kwargs = {}
> + def api_url(patch, version=None, item=None):
> + kwargs = {'patch_id': patch.id}
> if version:
> kwargs['version'] = version
> - kwargs['patch_id'] = patch.id
> + if item is None:
> + return reverse('api-patch-comment-list', kwargs=kwargs)
> + kwargs['comment_id'] = item.id
> + return reverse('api-patch-comment-detail', kwargs=kwargs)
>
> - return reverse('api-patch-comment-list', kwargs=kwargs)
> + def setUp(self):
> + super(TestPatchComments, self).setUp()
> + self.project = create_project()
> + self.user = create_maintainer(self.project)
> + self.patch = create_patch(project=self.project)
>
> def assertSerialized(self, comment_obj, comment_json):
> self.assertEqual(comment_obj.id, comment_json['id'])
> self.assertEqual(comment_obj.submitter.id,
> comment_json['submitter']['id'])
> + self.assertEqual(comment_obj.addressed, comment_json['addressed'])
> self.assertIn(SAMPLE_CONTENT, comment_json['content'])
>
> def test_list_empty(self):
> """List patch comments when none are present."""
> - patch = create_patch()
> - resp = self.client.get(self.api_url(patch))
> + resp = self.client.get(self.api_url(self.patch))
> self.assertEqual(status.HTTP_200_OK, resp.status_code)
> self.assertEqual(0, len(resp.data))
>
> @utils.store_samples('patch-comment-list')
> def test_list(self):
> """List patch comments."""
> - patch = create_patch()
> - comment = create_patch_comment(patch=patch)
> + comment = create_patch_comment(patch=self.patch)
>
> - resp = self.client.get(self.api_url(patch))
> + resp = self.client.get(self.api_url(self.patch))
> self.assertEqual(status.HTTP_200_OK, resp.status_code)
> self.assertEqual(1, len(resp.data))
> self.assertSerialized(comment, resp.data[0])
> @@ -121,26 +132,180 @@ class TestPatchComments(utils.APITestCase):
>
> def test_list_version_1_1(self):
> """List patch comments using API v1.1."""
> - patch = create_patch()
> - comment = create_patch_comment(patch=patch)
> + comment = create_patch_comment(patch=self.patch)
>
> - resp = self.client.get(self.api_url(patch, version='1.1'))
> + resp = self.client.get(self.api_url(self.patch, version='1.1'))
> self.assertEqual(status.HTTP_200_OK, resp.status_code)
> self.assertEqual(1, len(resp.data))
> self.assertSerialized(comment, resp.data[0])
> self.assertNotIn('list_archive_url', resp.data[0])
>
> def test_list_version_1_0(self):
> - """List patch comments using API v1.0."""
> - patch = create_patch()
> - create_patch_comment(patch=patch)
> + """List patch comments using API v1.0.
>
> - # check we can't access comments using the old version of the API
> + Ensure we can't access comments using the old version of the API.
> + """
> with self.assertRaises(NoReverseMatch):
> - self.client.get(self.api_url(patch, version='1.0'))
> + self.client.get(self.api_url(self.patch, version='1.0'))
>
> def test_list_invalid_patch(self):
> """Ensure we get a 404 for a non-existent patch."""
> resp = self.client.get(
> reverse('api-patch-comment-list', kwargs={'patch_id': '99999'}))
> self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code)
> +
> + @utils.store_samples('patch-comment-detail')
> + def test_detail(self):
> + """Show a patch comment."""
> + comment = create_patch_comment(patch=self.patch)
> +
> + resp = self.client.get(self.api_url(self.patch, item=comment))
> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
> + self.assertSerialized(comment, resp.data)
> +
> + def test_detail_version_1_3(self):
> + """Show a patch comment using API v1.3."""
> + comment = create_patch_comment(patch=self.patch)
> +
> + resp = self.client.get(
> + self.api_url(self.patch, version='1.3', item=comment))
> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
> + self.assertSerialized(comment, resp.data)
> +
> + def test_detail_version_1_2(self):
> + """Show a patch comment using API v1.2."""
> + comment = create_patch_comment(patch=self.patch)
> +
> + with self.assertRaises(NoReverseMatch):
> + self.client.get(
> + self.api_url(self.patch, version='1.2', item=comment))
> +
> + def test_detail_version_1_1(self):
> + """Show a patch comment using API v1.1."""
> + comment = create_patch_comment(patch=self.patch)
> +
> + with self.assertRaises(NoReverseMatch):
> + self.client.get(
> + self.api_url(self.patch, version='1.1', item=comment))
> +
> + def test_detail_version_1_0(self):
> + """Show a patch comment using API v1.0."""
> + comment = create_patch_comment(patch=self.patch)
> +
> + with self.assertRaises(NoReverseMatch):
> + self.client.get(
> + self.api_url(self.patch, version='1.0', item=comment))
> +
> + @utils.store_samples('patch-comment-detail-error-not-found')
> + def test_detail_invalid_patch(self):
> + """Ensure we handle non-existent patches."""
> + comment = create_patch_comment()
> + resp = self.client.get(
> + reverse('api-patch-comment-detail', kwargs={
> + 'patch_id': '99999',
> + 'comment_id': comment.id}
> + ),
> + )
> + self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code)
> +
> + def _test_update(self, person, **kwargs):
> + submitter = kwargs.get('submitter', person)
> + patch = kwargs.get('patch', self.patch)
> + comment = create_patch_comment(submitter=submitter, patch=patch)
> +
> + if kwargs.get('authenticate', True):
> + self.client.force_authenticate(user=person.user)
> + return self.client.patch(
> + self.api_url(patch, item=comment),
> + {'addressed': kwargs.get('addressed', True)},
> + validate_request=kwargs.get('validate_request', True)
> + )
> +
> + @utils.store_samples('patch-comment-detail-update-authorized')
> + def test_update_authorized(self):
> + """Update an existing patch comment as an authorized user.
> +
> + To be authorized users must meet at least one of the following:
> + - project maintainer, patch submitter, patch delegate, or
> + patch comment submitter
> +
> + Ensure updates can only be performed by authorized users.
> + """
> + # Update as maintainer
> + person = create_person(user=self.user)
> + resp = self._test_update(person=person)
> + self.assertEqual(1, PatchComment.objects.all().count())
> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
> + self.assertTrue(resp.data['addressed'])
> +
> + # Update as patch submitter
> + person = create_person(name='patch-submitter', user=create_user())
> + patch = create_patch(submitter=person)
> + resp = self._test_update(person=person, patch=patch)
> + self.assertEqual(2, PatchComment.objects.all().count())
> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
> + self.assertTrue(resp.data['addressed'])
> +
> + # Update as patch delegate
> + person = create_person(name='patch-delegate', user=create_user())
> + patch = create_patch(delegate=person.user)
> + resp = self._test_update(person=person, patch=patch)
> + self.assertEqual(3, PatchComment.objects.all().count())
> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
> + self.assertTrue(resp.data['addressed'])
> +
> + # Update as patch comment submitter
> + person = create_person(name='comment-submitter', user=create_user())
> + patch = create_patch()
> + resp = self._test_update(person=person, patch=patch, submitter=person)
> + self.assertEqual(4, PatchComment.objects.all().count())
> + self.assertEqual(status.HTTP_200_OK, resp.status_code)
> + self.assertTrue(resp.data['addressed'])
> +
> + @utils.store_samples('patch-comment-detail-update-not-authorized')
> + def test_update_not_authorized(self):
> + """Update an existing patch comment when not signed in and not authorized.
> +
> + To be authorized users must meet at least one of the following:
> + - project maintainer, patch submitter, patch delegate, or
> + patch comment submitter
> +
> + Ensure updates can only be performed by authorized users.
> + """
> + person = create_person(user=self.user)
> + resp = self._test_update(person=person, authenticate=False)
> + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
> +
> + person = create_person() # normal user without edit permissions
> + resp = self._test_update(person=person) # signed-in
> + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
> +
> + @utils.store_samples('patch-comment-detail-update-error-bad-request')
> + def test_update_invalid_addressed(self):
> + """Update an existing patch comment using invalid values.
> +
> + Ensure we handle invalid patch comment addressed values.
> + """
> + person = create_person(name='patch-submitter', user=create_user())
> + patch = create_patch(submitter=person)
> + resp = self._test_update(person=person,
> + patch=patch,
> + addressed='not-valid',
> + validate_request=False)
> + self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
> + self.assertFalse(
> + getattr(PatchComment.objects.all().first(), 'addressed')
> + )
> +
> + def test_create_delete(self):
> + """Ensure creates and deletes aren't allowed"""
> + comment = create_patch_comment(patch=self.patch)
> + self.user.is_superuser = True
> + self.user.save()
> + self.client.force_authenticate(user=self.user)
> +
> + resp = self.client.post(self.api_url(self.patch, item=comment))
> + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
> +
> + resp = self.client.delete(self.api_url(self.patch, item=comment))
> + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
> diff --git a/patchwork/urls.py b/patchwork/urls.py
> index 1e6c12a..0c48727 100644
> --- a/patchwork/urls.py
> +++ b/patchwork/urls.py
> @@ -343,12 +343,23 @@ if settings.ENABLE_REST_API:
> ),
> ]
>
> + api_1_3_patterns = [
> + path(
> + 'patches/<patch_id>/comments/<comment_id>/',
> + api_comment_views.PatchCommentDetail.as_view(),
> + name='api-patch-comment-detail',
> + ),
> + ]
> +
> urlpatterns += [
> re_path(
> - r'^api/(?:(?P<version>(1.0|1.1|1.2))/)?', include(api_patterns)
> + r'^api/(?:(?P<version>(1.0|1.1|1.2|1.3))/)?', include(api_patterns)
> + ),
> + re_path(
> + r'^api/(?:(?P<version>(1.1|1.2|1.3))/)?', include(api_1_1_patterns)
> ),
> re_path(
> - r'^api/(?:(?P<version>(1.1|1.2))/)?', include(api_1_1_patterns)
> + r'^api/(?:(?P<version>(1.3))/)?', include(api_1_3_patterns)
> ),
> # token change
> path(
> --
> 2.33.0.rc1.237.g0d66db33f3-goog
>
> _______________________________________________
> Patchwork mailing list
> Patchwork at lists.ozlabs.org
> https://lists.ozlabs.org/listinfo/patchwork
More information about the Patchwork
mailing list