[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