Raxel Gutierrez <ra...@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 <ra...@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@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@lists.ozlabs.org > https://lists.ozlabs.org/listinfo/patchwork _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork