diff --git a/docs/api/schemas/latest/patchwork.yaml b/docs/api/schemas/latest/patchwork.yaml index b2bb220f..d8cfa577 100644 --- a/docs/api/schemas/latest/patchwork.yaml +++ b/docs/api/schemas/latest/patchwork.yaml @@ -319,6 +319,19 @@ paths: - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/Order' - $ref: '#/components/parameters/Search' + - in: query + name: type + required: false + description: | + Filter comment type. Use 'notes' to show only maintainer notes, + 'comments' to show only regular comments, or omit for default behavior + (which only shows regular comments). + schema: + title: 'Comment type' + type: string + enum: + - notes + - comments responses: '200': description: 'List of comments' @@ -339,6 +352,43 @@ paths: $ref: '#/components/schemas/Error' tags: - comments + post: + summary: Create a maintainer note in the cover + description: | + Create a maintainer note in the cover + operationId: cover_maintainer_note_create + requestBody: + $ref: '#/components/requestBodies/MaintainerNote' + responses: + '201': + description: 'Created maintainer note' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: 'Invalid request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMaintainerNote' + '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/covers/{cover_id}/comments/{comment_id}: parameters: - in: path @@ -356,7 +406,7 @@ paths: title: Comment ID type: integer get: - summary: Show a cover letter comment. + summary: Show a cover letter comment or maintainer note. description: | Retrieve a cover letter comment by its ID. operationId: cover_comments_read @@ -376,7 +426,7 @@ paths: tags: - comments patch: - summary: Update a cover letter comment (partial). + summary: Update a cover letter comment or maintainer note (partial). description: Partially update an existing cover letter comment. You must be a maintainer of the project that the cover letter comment belongs to. @@ -725,6 +775,19 @@ paths: - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/Order' - $ref: '#/components/parameters/Search' + - in: query + name: type + required: false + description: | + Filter comment type. Use 'notes' to show only maintainer notes, + 'comments' to show only regular comments, or omit for default behavior + (which only shows regular comments). + schema: + title: 'Comment type' + type: string + enum: + - notes + - comments responses: '200': description: 'List of comments' @@ -745,6 +808,43 @@ paths: $ref: '#/components/schemas/Error' tags: - comments + post: + summary: Create a maintainer note in the patch + description: | + Create a maintainer note in the patch + operationId: patch_maintainer_note_create + requestBody: + $ref: '#/components/requestBodies/MaintainerNote' + responses: + '201': + description: 'Created maintainer note' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: 'Invalid request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMaintainerNote' + '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}/comments/{comment_id}: parameters: - in: path @@ -762,7 +862,7 @@ paths: title: Comment ID type: integer get: - summary: Show a patch comment. + summary: Show a patch comment or maintainer note. description: | Retrieve a patch comment by its ID and the ID of the patch. operationId: patch_comments_read @@ -782,7 +882,7 @@ paths: tags: - comments patch: - summary: Update a patch comment (partial). + summary: Update a patch comment or mantainer note (partial). description: Partially update an existing patch comment. You must be a maintainer of the project that the patch comment belongs to. @@ -1478,6 +1578,14 @@ components: application/json: schema: $ref: '#/components/schemas/CommentUpdate' + MaintainerNote: + required: true + description: | + A maintainer note for patch or cover letters. + content: + application/json: + schema: + $ref: '#/components/schemas/MaintainerNote' Patch: required: true description: | @@ -1770,7 +1878,6 @@ components: title: Message ID type: string readOnly: true - minLength: 1 maxLength: 255 list_archive_url: title: List archive URL @@ -1801,7 +1908,6 @@ components: content: title: Content type: string - readOnly: true minLength: 1 headers: title: Headers @@ -1831,6 +1937,20 @@ components: type: - 'null' - 'boolean' + MaintainerNote: + type: object + title: Maintainer note + description: | + The fields to set on a maintainer note. + properties: + content: + title: Content + type: string + addressed: + title: Addressed + type: + - 'null' + - 'boolean' CoverList: type: object title: Cover letters @@ -3153,6 +3273,22 @@ components: type: array items: type: string + ErrorMaintainerNote: + type: object + title: A maintainer note create or update error. + description: | + A mapping of field names to validation failures. + properties: + content: + title: Content + type: array + items: + type: string + addressed: + title: Addressed + type: array + items: + type: string ErrorPatchUpdate: type: object title: A patch update error. diff --git a/docs/api/schemas/patchwork.j2 b/docs/api/schemas/patchwork.j2 index f37d3213..f8c9cff7 100644 --- a/docs/api/schemas/patchwork.j2 +++ b/docs/api/schemas/patchwork.j2 @@ -330,6 +330,21 @@ paths: - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/Order' - $ref: '#/components/parameters/Search' +{% if version >= (1, 4) %} + - in: query + name: type + required: false + description: | + Filter comment type. Use 'notes' to show only maintainer notes, + 'comments' to show only regular comments, or omit for default behavior + (which only shows regular comments). + schema: + title: 'Comment type' + type: string + enum: + - notes + - comments +{% endif %} responses: '200': description: 'List of comments' @@ -350,6 +365,45 @@ paths: $ref: '#/components/schemas/Error' tags: - comments +{% if version >= (1, 4) %} + post: + summary: Create a maintainer note in the cover + description: | + Create a maintainer note in the cover + operationId: cover_maintainer_note_create + requestBody: + $ref: '#/components/requestBodies/MaintainerNote' + responses: + '201': + description: 'Created maintainer note' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: 'Invalid request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMaintainerNote' + '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 %} {% if version >= (1, 3) %} /api/{{ version_url }}covers/{cover_id}/comments/{comment_id}: parameters: @@ -368,7 +422,7 @@ paths: title: Comment ID type: integer get: - summary: Show a cover letter comment. + summary: Show a cover letter comment or maintainer note. description: | Retrieve a cover letter comment by its ID. operationId: cover_comments_read @@ -388,7 +442,7 @@ paths: tags: - comments patch: - summary: Update a cover letter comment (partial). + summary: Update a cover letter comment or maintainer note (partial). description: Partially update an existing cover letter comment. You must be a maintainer of the project that the cover letter comment belongs to. @@ -748,6 +802,21 @@ paths: - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/Order' - $ref: '#/components/parameters/Search' +{% if version >= (1, 4) %} + - in: query + name: type + required: false + description: | + Filter comment type. Use 'notes' to show only maintainer notes, + 'comments' to show only regular comments, or omit for default behavior + (which only shows regular comments). + schema: + title: 'Comment type' + type: string + enum: + - notes + - comments +{% endif %} responses: '200': description: 'List of comments' @@ -768,6 +837,45 @@ paths: $ref: '#/components/schemas/Error' tags: - comments +{% if version >= (1, 4) %} + post: + summary: Create a maintainer note in the patch + description: | + Create a maintainer note in the patch + operationId: patch_maintainer_note_create + requestBody: + $ref: '#/components/requestBodies/MaintainerNote' + responses: + '201': + description: 'Created maintainer note' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: 'Invalid request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMaintainerNote' + '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 %} {% if version >= (1, 3) %} /api/{{ version_url }}patches/{patch_id}/comments/{comment_id}: parameters: @@ -786,7 +894,7 @@ paths: title: Comment ID type: integer get: - summary: Show a patch comment. + summary: Show a patch comment or maintainer note. description: | Retrieve a patch comment by its ID and the ID of the patch. operationId: patch_comments_read @@ -806,7 +914,7 @@ paths: tags: - comments patch: - summary: Update a patch comment (partial). + summary: Update a patch comment or mantainer note (partial). description: Partially update an existing patch comment. You must be a maintainer of the project that the patch comment belongs to. @@ -1518,6 +1626,16 @@ components: application/json: schema: $ref: '#/components/schemas/CommentUpdate' +{% endif %} +{% if version >= (1, 4) %} + MaintainerNote: + required: true + description: | + A maintainer note for patch or cover letters. + content: + application/json: + schema: + $ref: '#/components/schemas/MaintainerNote' {% endif %} Patch: required: true @@ -1834,7 +1952,9 @@ components: title: Message ID type: string readOnly: true +{% if version < (1, 4) %} minLength: 1 +{% endif %} maxLength: 255 {% if version >= (1, 2) %} list_archive_url: @@ -1867,7 +1987,9 @@ components: content: title: Content type: string +{% if version < (1, 4) %} readOnly: true +{% endif %} minLength: 1 headers: title: Headers @@ -1898,6 +2020,22 @@ components: type: - 'null' - 'boolean' +{% endif %} +{% if version >= (1, 4) %} + MaintainerNote: + type: object + title: Maintainer note + description: | + The fields to set on a maintainer note. + properties: + content: + title: Content + type: string + addressed: + title: Addressed + type: + - 'null' + - 'boolean' {% endif %} CoverList: type: object @@ -3274,6 +3412,24 @@ components: type: array items: type: string +{% endif %} +{% if version >= (1, 4) %} + ErrorMaintainerNote: + type: object + title: A maintainer note create or update error. + description: | + A mapping of field names to validation failures. + properties: + content: + title: Content + type: array + items: + type: string + addressed: + title: Addressed + type: array + items: + type: string {% endif %} ErrorPatchUpdate: type: object diff --git a/docs/api/schemas/v1.3/patchwork.yaml b/docs/api/schemas/v1.3/patchwork.yaml index 41b44832..c6d0f3d7 100644 --- a/docs/api/schemas/v1.3/patchwork.yaml +++ b/docs/api/schemas/v1.3/patchwork.yaml @@ -356,7 +356,7 @@ paths: title: Comment ID type: integer get: - summary: Show a cover letter comment. + summary: Show a cover letter comment or maintainer note. description: | Retrieve a cover letter comment by its ID. operationId: cover_comments_read @@ -376,7 +376,7 @@ paths: tags: - comments patch: - summary: Update a cover letter comment (partial). + summary: Update a cover letter comment or maintainer note (partial). description: Partially update an existing cover letter comment. You must be a maintainer of the project that the cover letter comment belongs to. @@ -762,7 +762,7 @@ paths: title: Comment ID type: integer get: - summary: Show a patch comment. + summary: Show a patch comment or maintainer note. description: | Retrieve a patch comment by its ID and the ID of the patch. operationId: patch_comments_read @@ -782,7 +782,7 @@ paths: tags: - comments patch: - summary: Update a patch comment (partial). + summary: Update a patch comment or mantainer note (partial). description: Partially update an existing patch comment. You must be a maintainer of the project that the patch comment belongs to. diff --git a/docs/api/schemas/v1.4/patchwork.yaml b/docs/api/schemas/v1.4/patchwork.yaml index 036fe15f..6c1a43b3 100644 --- a/docs/api/schemas/v1.4/patchwork.yaml +++ b/docs/api/schemas/v1.4/patchwork.yaml @@ -319,6 +319,19 @@ paths: - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/Order' - $ref: '#/components/parameters/Search' + - in: query + name: type + required: false + description: | + Filter comment type. Use 'notes' to show only maintainer notes, + 'comments' to show only regular comments, or omit for default behavior + (which only shows regular comments). + schema: + title: 'Comment type' + type: string + enum: + - notes + - comments responses: '200': description: 'List of comments' @@ -339,6 +352,43 @@ paths: $ref: '#/components/schemas/Error' tags: - comments + post: + summary: Create a maintainer note in the cover + description: | + Create a maintainer note in the cover + operationId: cover_maintainer_note_create + requestBody: + $ref: '#/components/requestBodies/MaintainerNote' + responses: + '201': + description: 'Created maintainer note' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: 'Invalid request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMaintainerNote' + '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.4/covers/{cover_id}/comments/{comment_id}: parameters: - in: path @@ -356,7 +406,7 @@ paths: title: Comment ID type: integer get: - summary: Show a cover letter comment. + summary: Show a cover letter comment or maintainer note. description: | Retrieve a cover letter comment by its ID. operationId: cover_comments_read @@ -376,7 +426,7 @@ paths: tags: - comments patch: - summary: Update a cover letter comment (partial). + summary: Update a cover letter comment or maintainer note (partial). description: Partially update an existing cover letter comment. You must be a maintainer of the project that the cover letter comment belongs to. @@ -725,6 +775,19 @@ paths: - $ref: '#/components/parameters/PageSize' - $ref: '#/components/parameters/Order' - $ref: '#/components/parameters/Search' + - in: query + name: type + required: false + description: | + Filter comment type. Use 'notes' to show only maintainer notes, + 'comments' to show only regular comments, or omit for default behavior + (which only shows regular comments). + schema: + title: 'Comment type' + type: string + enum: + - notes + - comments responses: '200': description: 'List of comments' @@ -745,6 +808,43 @@ paths: $ref: '#/components/schemas/Error' tags: - comments + post: + summary: Create a maintainer note in the patch + description: | + Create a maintainer note in the patch + operationId: patch_maintainer_note_create + requestBody: + $ref: '#/components/requestBodies/MaintainerNote' + responses: + '201': + description: 'Created maintainer note' + headers: + Link: + $ref: '#/components/headers/Link' + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: 'Invalid request' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMaintainerNote' + '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.4/patches/{patch_id}/comments/{comment_id}: parameters: - in: path @@ -762,7 +862,7 @@ paths: title: Comment ID type: integer get: - summary: Show a patch comment. + summary: Show a patch comment or maintainer note. description: | Retrieve a patch comment by its ID and the ID of the patch. operationId: patch_comments_read @@ -782,7 +882,7 @@ paths: tags: - comments patch: - summary: Update a patch comment (partial). + summary: Update a patch comment or mantainer note (partial). description: Partially update an existing patch comment. You must be a maintainer of the project that the patch comment belongs to. @@ -1478,6 +1578,14 @@ components: application/json: schema: $ref: '#/components/schemas/CommentUpdate' + MaintainerNote: + required: true + description: | + A maintainer note for patch or cover letters. + content: + application/json: + schema: + $ref: '#/components/schemas/MaintainerNote' Patch: required: true description: | @@ -1770,7 +1878,6 @@ components: title: Message ID type: string readOnly: true - minLength: 1 maxLength: 255 list_archive_url: title: List archive URL @@ -1801,7 +1908,6 @@ components: content: title: Content type: string - readOnly: true minLength: 1 headers: title: Headers @@ -1831,6 +1937,20 @@ components: type: - 'null' - 'boolean' + MaintainerNote: + type: object + title: Maintainer note + description: | + The fields to set on a maintainer note. + properties: + content: + title: Content + type: string + addressed: + title: Addressed + type: + - 'null' + - 'boolean' CoverList: type: object title: Cover letters @@ -3153,6 +3273,22 @@ components: type: array items: type: string + ErrorMaintainerNote: + type: object + title: A maintainer note create or update error. + description: | + A mapping of field names to validation failures. + properties: + content: + title: Content + type: array + items: + type: string + addressed: + title: Addressed + type: array + items: + type: string ErrorPatchUpdate: type: object title: A patch update error. diff --git a/htdocs/css/style.css b/htdocs/css/style.css index 268a8c37..6c2aeca8 100644 --- a/htdocs/css/style.css +++ b/htdocs/css/style.css @@ -325,6 +325,26 @@ table.patch-meta tr th, table.patch-meta tr td { color: #f7977a; } +.submission-message.maintainer a { + color: blue; +} + +.submission-message.maintainer div.text-warning { + color: yellow; +} + +.submission-message.maintainer div.text-success { + color: lightgreen; +} +.submission-message.maintainer .meta { + background-color: red; + color: white; +} + +.submission-message.maintainer .content { + background-color: #ff7d7d; +} + .submission-message .meta { display: flex; align-items: center; @@ -449,6 +469,16 @@ div.patch-form { align-items: center; } +#maintainer-note-form .form-actions { + display: flex; + justify-content: flex-start; /* Align buttons to the left */ + gap: 10px; /* Add space between buttons */ +} +#maintainer-note-form div:first-of-type { + display: flex; + flex-direction: column; +} + select[class^=change-property-], .archive-patch-select, .add-bundle { padding: 4px; margin-right: 8px; @@ -476,7 +506,7 @@ select[class^=change-property-], .archive-patch-select, .add-bundle { padding: 4px; } -#patch-form-bundle, #add-to-bundle, #remove-bundle { +#patch-form-bundle, #add-to-bundle, #remove-bundle, #note-button { margin-left: 16px; } diff --git a/patchwork/admin.py b/patchwork/admin.py index d1c389a1..b2cc5bff 100644 --- a/patchwork/admin.py +++ b/patchwork/admin.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later from django.contrib import admin +from django.contrib.admin import SimpleListFilter from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User from django.db.models import Prefetch @@ -112,7 +113,25 @@ def is_pull_request(self, patch): admin.site.register(Patch, PatchAdmin) +class MaintainerNoteFilter(SimpleListFilter): + title = 'comment type' + parameter_name = 'type' + + def lookups(self, request, model_admin): + return [ + ('comments', 'Comments'), + ('notes', 'Maintainer notes'), + ] + + def queryset(self, request, queryset): + if self.value() == 'notes': + return queryset.filter(msgid='') + + return queryset.exclude(msgid='') + + class CoverCommentAdmin(admin.ModelAdmin): + list_filter = [MaintainerNoteFilter] list_display = ('cover', 'submitter', 'date') search_fields = ('cover__name', 'submitter__name', 'submitter__email') date_hierarchy = 'date' @@ -122,6 +141,7 @@ class CoverCommentAdmin(admin.ModelAdmin): class PatchCommentAdmin(admin.ModelAdmin): + list_filter = [MaintainerNoteFilter] list_display = ('patch', 'submitter', 'date') search_fields = ('patch__name', 'submitter__name', 'submitter__email') date_hierarchy = 'date' diff --git a/patchwork/api/base.py b/patchwork/api/base.py index 16e5cb8d..546f0372 100644 --- a/patchwork/api/base.py +++ b/patchwork/api/base.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: GPL-2.0-or-later +from django.urls import resolve import rest_framework from django.conf import settings @@ -15,6 +16,8 @@ from rest_framework.utils.urls import replace_query_param from patchwork.api import utils +from patchwork.models import Cover +from patchwork.models import Patch DRF_VERSION = tuple(int(x) for x in rest_framework.__version__.split('.')) @@ -22,30 +25,69 @@ if DRF_VERSION > (3, 11): + class CurrentPersonDefault(object): + requires_context = True + + def __call__(self, serializer_field): + return ( + serializer_field.context['request'] + .user.person_set.all() + .first() + ) + class CurrentPatchDefault(object): requires_context = True def __call__(self, serializer_field): + req = serializer_field.context['request'] + _, _, kwargs = resolve(req.path) + if 'patch_id' in kwargs: + return get_object_or_404(Patch, id=kwargs['patch_id']) return serializer_field.context['request'].patch class CurrentCoverDefault(object): requires_context = True def __call__(self, serializer_field): - return serializer_field.context['request'].cover + req = serializer_field.context['request'] + _, _, kwargs = resolve(req.path) + if 'cover_id' in kwargs: + return get_object_or_404(Cover, id=kwargs['cover_id']) + return req.cover else: + class CurrentPersonDefault(object): + def set_context(self, serializer_field): + self.person = ( + serializer_field.context['request'] + .user.person_set.all() + .first() + ) + + def __call__(self): + return self.person + class CurrentPatchDefault(object): def set_context(self, serializer_field): - self.patch = serializer_field.context['request'].patch + req = serializer_field.context['request'] + _, _, kwargs = resolve(req.path) + if 'patch_id' in kwargs: + self.patch = get_object_or_404(Patch, id=kwargs['patch_id']) + else: + self.patch = req.patch def __call__(self): return self.patch class CurrentCoverDefault(object): def set_context(self, serializer_field): - self.patch = serializer_field.context['request'].cover + req = serializer_field.context['request'] + _, _, kwargs = resolve(req.path) + if 'cover_id' in kwargs: + self.cover = get_object_or_404(Cover, id=kwargs['cover_id']) + else: + self.cover = req.cover def __call__(self): return self.cover diff --git a/patchwork/api/comment.py b/patchwork/api/comment.py index 88707b84..ee518f1a 100644 --- a/patchwork/api/comment.py +++ b/patchwork/api/comment.py @@ -5,17 +5,26 @@ import email.parser +from rest_framework.generics import RetrieveUpdateDestroyAPIView from rest_framework.generics import get_object_or_404 -from rest_framework.generics import ListAPIView -from rest_framework.generics import RetrieveUpdateAPIView +from rest_framework.generics import ListCreateAPIView +from rest_framework.permissions import SAFE_METHODS +from rest_framework.serializers import CreateOnlyDefault +from rest_framework.serializers import CharField from rest_framework.serializers import HiddenField +from rest_framework.serializers import ValidationError from rest_framework.serializers import SerializerMethodField +from rest_framework.views import PermissionDenied +from rest_framework.exceptions import NotAuthenticated +from django_filters.rest_framework import ChoiceFilter +from django_filters.rest_framework import FilterSet from patchwork.api.base import BaseHyperlinkedModelSerializer +from patchwork.api.base import CurrentCoverDefault +from patchwork.api.base import CurrentPersonDefault from patchwork.api.base import NestedHyperlinkedIdentityField from patchwork.api.base import MultipleFieldLookupMixin from patchwork.api.base import PatchworkPermission -from patchwork.api.base import CurrentCoverDefault from patchwork.api.base import CurrentPatchDefault from patchwork.api.embedded import PersonSerializer from patchwork.models import Cover @@ -99,7 +108,7 @@ class CoverCommentSerializer(BaseCommentListSerializer): 'comment_id': 'id', }, ) - cover = HiddenField(default=CurrentCoverDefault()) + cover = HiddenField(default=CreateOnlyDefault(CurrentCoverDefault())) class Meta: model = CoverComment @@ -111,6 +120,38 @@ class Meta: extra_kwargs = {'url': {'view_name': 'api-cover-comment-detail'}} +class CoverMaintainerNoteSerializer(CoverCommentSerializer): + content = CharField(required=True) + submitter = PersonSerializer( + read_only=True, default=CreateOnlyDefault(CurrentPersonDefault()) + ) + + def validate(self, attrs): + is_create = self.instance is None + if is_create: + # ReadOnly fields are ignored in create/update operations + submitter_field = self.fields.get('submitter') + attrs['submitter'] = submitter_field.default(submitter_field) + + if self.Meta.model.objects.filter( + cover=attrs['cover'], msgid='' + ).first(): + raise ValidationError( + 'Maintaner note already exists for cover' + ) + + return super().validate(attrs) + + class Meta: + model = CoverComment + fields = CoverCommentSerializer.Meta.fields + read_only_fields = tuple( + f + for f in CoverCommentSerializer.Meta.read_only_fields + if f not in ('content',) + ) + + class CoverCommentMixin(object): permission_classes = (PatchworkPermission,) serializer_class = CoverCommentSerializer @@ -139,7 +180,7 @@ class PatchCommentSerializer(BaseCommentListSerializer): 'comment_id': 'id', }, ) - patch = HiddenField(default=CurrentPatchDefault()) + patch = HiddenField(default=CreateOnlyDefault(CurrentPatchDefault())) class Meta: model = PatchComment @@ -151,6 +192,38 @@ class Meta: extra_kwargs = {'url': {'view_name': 'api-patch-comment-detail'}} +class PatchMaintainerNoteSerializer(PatchCommentSerializer): + content = CharField(required=True) + submitter = PersonSerializer( + read_only=True, default=CreateOnlyDefault(CurrentPersonDefault()) + ) + + def validate(self, attrs): + is_create = self.instance is None + if is_create: + # ReadOnly fields are ignored in create/update operations + submitter_field = self.fields.get('submitter') + attrs['submitter'] = submitter_field.default(submitter_field) + + if self.Meta.model.objects.filter( + patch=attrs['patch'], msgid='' + ).first(): + raise ValidationError( + 'Maintaner note already exists for patch' + ) + + return super().validate(attrs) + + class Meta: + model = PatchComment + fields = PatchCommentSerializer.Meta.fields + read_only_fields = tuple( + f + for f in PatchCommentSerializer.Meta.read_only_fields + if f not in ('content',) + ) + + class PatchCommentMixin(object): permission_classes = (PatchworkPermission,) serializer_class = PatchCommentSerializer @@ -171,55 +244,191 @@ def get_queryset(self): ) -class CoverCommentList(CoverCommentMixin, ListAPIView): - """List cover comments""" +COMMENT_TYPES = ( + ('comments', 'Comments'), + ('notes', 'Maintainer Notes'), +) + + +class CommentsFilter(FilterSet): + type = ChoiceFilter( + label='Type', + field_name='msgid', + choices=COMMENT_TYPES, + method='filter_choice', + ) + + def filter_choice(self, queryset, name, value): + if name == 'msgid' and value == 'notes': + return queryset.filter(msgid='') + + return queryset.exclude(msgid='') + + class Meta: + fields = ['msgid'] + + +class MaintainerNoteMixin(object): + """ + Only maintainers can create objects and when creating or updating a note + use the correct serializer. + + Override `maintainer_note_serializer_class` with the serializer that must + be used and `project_lookup_attr` with the validated data key that holds a + relationship with the object project. + """ + + maintainer_note_serializer_class = None + project_lookup_attr = '' + filterset_class = CommentsFilter + _ERROR_MSG_MAP = {'DELETE': 'delete', 'POST': 'create'} + + def get_queryset(self): + """ + We always remove maintainer notes from the queryset, unless specified + by the filter type. + """ + qs = super(MaintainerNoteMixin, self).get_queryset() + if ( + self.request.method in SAFE_METHODS + and not self.request.query_params.get('type') + ): + return qs.exclude(msgid='') + return qs + + def get_serializer_class(self): + assert self.maintainer_note_serializer_class is not None + + if self.request.method in ('PUT', 'PATCH'): + obj = self.get_object() + if obj and obj.is_maintainer_note: + return self.maintainer_note_serializer_class + + if self.request.method == 'POST': + return self.maintainer_note_serializer_class + + return super(MaintainerNoteMixin, self).get_serializer_class() + + def _check_notes_permission(self, instance, user): + assert self.project_lookup_attr != '' + try: + project = instance[self.project_lookup_attr].project + msgid = instance.get('msgid', '') + except (TypeError, KeyError): + project = getattr(instance, self.project_lookup_attr).project + msgid = instance.msgid + + action_msg = self._ERROR_MSG_MAP[self.request.method] + if not user or not user.is_authenticated: + raise NotAuthenticated( + f'You must be authenticated to {action_msg} a maintainer note' + ) + + if msgid: + raise PermissionDenied( + f'Only maintainer notes can be {action_msg}d' + ) + if not project.is_editable(user): + raise PermissionDenied( + f'You must be a maintainer to {action_msg} a note' + ) + + def perform_create(self, serializer): + self._check_notes_permission( + serializer.validated_data, self.request.user + ) + return super(MaintainerNoteMixin, self).perform_create(serializer) + + def perform_destroy(self, instance): + self._check_notes_permission(instance, self.request.user) + return super(MaintainerNoteMixin, self).perform_destroy(instance) + + +class CoverCommentList( + MaintainerNoteMixin, CoverCommentMixin, ListCreateAPIView +): + """ + get: + List cover comments + + post: + Create a maintainer note in the cover + """ search_fields = ('subject',) ordering_fields = ('id', 'subject', 'date', 'submitter') ordering = 'id' lookup_url_kwarg = 'cover_id' + maintainer_note_serializer_class = CoverMaintainerNoteSerializer + project_lookup_attr = 'cover' class CoverCommentDetail( - CoverCommentMixin, MultipleFieldLookupMixin, RetrieveUpdateAPIView + MaintainerNoteMixin, + CoverCommentMixin, + MultipleFieldLookupMixin, + RetrieveUpdateDestroyAPIView, ): """ get: Show a cover comment. patch: - Update a cover comment. + Update a cover comment or maintainer note. put: - Update a cover comment. + Update a cover comment or maintainer note. + + delete: + Delete a maintainer note. """ lookup_url_kwargs = ('cover_id', 'comment_id') lookup_fields = ('cover_id', 'id') + maintainer_note_serializer_class = CoverMaintainerNoteSerializer + project_lookup_attr = 'cover' -class PatchCommentList(PatchCommentMixin, ListAPIView): - """List patch comments""" +class PatchCommentList( + MaintainerNoteMixin, PatchCommentMixin, ListCreateAPIView +): + """ + get: + List patch comments + + post: + Create a maintainer note in the patch + """ search_fields = ('subject',) ordering_fields = ('id', 'subject', 'date', 'submitter') ordering = 'id' lookup_url_kwarg = 'patch_id' + maintainer_note_serializer_class = PatchMaintainerNoteSerializer + project_lookup_attr = 'patch' class PatchCommentDetail( - PatchCommentMixin, MultipleFieldLookupMixin, RetrieveUpdateAPIView + MaintainerNoteMixin, + PatchCommentMixin, + MultipleFieldLookupMixin, + RetrieveUpdateDestroyAPIView, ): """ get: Show a patch comment. patch: - Update a patch comment. + Update a patch comment or maintainer note. put: - Update a patch comment. + Update a patch comment or maintainer note. + + delete: + Delete a maintainer note. """ lookup_url_kwargs = ('patch_id', 'comment_id') lookup_fields = ('patch_id', 'id') + maintainer_note_serializer_class = PatchMaintainerNoteSerializer + project_lookup_attr = 'patch' diff --git a/patchwork/forms.py b/patchwork/forms.py index cf77bdcc..8b943a8c 100644 --- a/patchwork/forms.py +++ b/patchwork/forms.py @@ -12,6 +12,7 @@ from django.template.backends import django as django_template_backend from patchwork.models import Bundle +from patchwork.models import PatchComment from patchwork.models import Patch from patchwork.models import State from patchwork.models import UserProfile @@ -106,6 +107,19 @@ class DeleteBundleForm(forms.Form): bundle_id = forms.IntegerField(widget=forms.HiddenInput) +class PatchMaintainerNoteForm(forms.ModelForm): + name = 'patchnoteform' + form_name = forms.CharField(initial=name, widget=forms.HiddenInput) + content = forms.CharField(label='Content', widget=forms.Textarea) + addressed = forms.BooleanField( + label='Addressed', widget=forms.CheckboxInput, required=False + ) + + class Meta: + model = PatchComment + fields = ['content', 'addressed'] + + class EmailForm(forms.Form): email = forms.EmailField(max_length=200) diff --git a/patchwork/models.py b/patchwork/models.py index ae2f4a6d..bb7da160 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -360,6 +360,10 @@ class EmailMixin(models.Model): re.M | re.I, ) + @property + def is_maintainer_note(self): + return self.msgid == '' + @property def patch_responses(self): if not self.content: diff --git a/patchwork/templates/patchwork/partials/comment.html b/patchwork/templates/patchwork/partials/comment.html new file mode 100644 index 00000000..ed01122a --- /dev/null +++ b/patchwork/templates/patchwork/partials/comment.html @@ -0,0 +1,66 @@ +{% load person %} +{% load utils %} +{% load syntax %} + +{% is_editable submission user as editable %} +{% is_editable item user as comment_is_editable %} + +
diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html index cd74491c..1aa78181 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -136,6 +136,25 @@ {% endif %}