From patchwork Fri Aug 20 04:50:22 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1518910 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org; envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org; receiver=) Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20161025 header.b=DAPYyyz2; dkim-atps=neutral Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4GrTjM5hW6z9sWl for ; Fri, 20 Aug 2021 14:50:47 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4GrTjL0VQZz3bmn for ; Fri, 20 Aug 2021 14:50:46 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20161025 header.b=DAPYyyz2; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=flex--raxel.bounces.google.com (client-ip=2607:f8b0:4864:20::849; helo=mail-qt1-x849.google.com; envelope-from=3mzqfyqukczabuhy508805y.w869udw1g8b452cdc.8j5uvc.8b0@flex--raxel.bounces.google.com; receiver=) Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20161025 header.b=DAPYyyz2; dkim-atps=neutral Received: from mail-qt1-x849.google.com (mail-qt1-x849.google.com [IPv6:2607:f8b0:4864:20::849]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjC3sCpz30JT for ; Fri, 20 Aug 2021 14:50:37 +1000 (AEST) Received: by mail-qt1-x849.google.com with SMTP id r5-20020ac85e85000000b0029bd6ee5179so3670029qtx.18 for ; Thu, 19 Aug 2021 21:50:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=X+qVlcbb0i/3GChV5fADqPJvYfEbyzFaCXwpgH0oN2w=; b=DAPYyyz26v8AbOFrAdFXRg0hW9h9Omc4gDRfBk6WDBCoKqxI2NPHYb2oR9UXkbZUwI d24/NQgu8satpsm1JrKBHKI5dY+x0MWnQVzRhaq1KC+eo8z1vqHkjwYfNbElphw2uuIy h9IH1s6vQIEapTxFDu07/yoKjGbLuMqx+teh7yoB8ZtIF4gMyyOCaNpN2jE8evm0L2Uy 06O3hidXI5x+a48O04I1/4Cba+JyQYo1kKp63BnOGp/m1mHtujD8zjGKAAPUpcF4vDth jvta5u6Yzb2SfDI14UFJ9NgXLNsCl1cHdOPPi3fl14L9sHYrG+5DnOyYevK1uqYhTNBH YZzg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=X+qVlcbb0i/3GChV5fADqPJvYfEbyzFaCXwpgH0oN2w=; b=rCyxDJWl321gFwFBJTMzTbOZIUVE6ldErs26PmG//sAsVcLowvJKMhGQ39x3U11L0A m3e6XUeeJPF+LHT8heNYryJ4myt+ijizhBOUxLpzTeqP6LIgS5w/RWKzCuf5yoRI090k fZk/GJzo4OBxT0Uq81OUwkW22cQvSlxUtmKZ2kwHshO/z1xoL6Emf+mlJ4Gga0FBHJjt 8aIiM3Ix5KBmptHrdHlkqVEzQD8RoXOusdj3d+Gh+xrn/xz1ivuhILMmkjLVEFz3QssQ WPofpvRY78LHcQ3rRmSt2jmQN5siHiLPS9z9QaFrTCUBo1IXDLIkmytSeoyZcr+Fj022 lIew== X-Gm-Message-State: AOAM5330aUBLMb1sJKU0RDKpK4sYGHRcYX5aA3URywCvK2tpFOGbWJNs VabtZv6mT0Yl0fyz/PU3QGnHDDahpBlihs9hYaL0Sx8NOBBiNuv9Ui1ZqFeHjZCYxYbmyl40JqU 8Iiq0XOdilhRrJ8Wtz6Q3XxPXOh4ifAb7RcpA0H/eAW6foBz8VBbm8/tR6SnX+yJQ X-Google-Smtp-Source: ABdhPJyvX9SfNc7JRw1Hz5HJQVDb/P1HdPz2qNYxSnqEP2g0n8E1MVwztIJe6vjnlyqxRFzWmAtvDSbTMg== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:cc9:: with SMTP id 9mr17924292qvx.43.1629435035004; Thu, 19 Aug 2021 21:50:35 -0700 (PDT) Date: Fri, 20 Aug 2021 04:50:22 +0000 In-Reply-To: <20210820045030.3364156-1-raxel@google.com> Message-Id: <20210820045030.3364156-2-raxel@google.com> Mime-Version: 1.0 References: <20210820045030.3364156-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [PATCH v4 1/9] patch-detail: left align message headers From: Raxel Gutierrez To: patchwork@lists.ozlabs.org X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" Change both of the message containers in the "Commit Message" and "Comments" to have their content be left-aligned which improves the proximity of items which boosts the efficiency of gathering information and performing actions. Before [1] and after [2] images for reference. [1] https://i.imgur.com/ji2ZINL.png [2] https://i.imgur.com/Dtn8lm9.png Signed-off-by: Raxel Gutierrez Reviewed-by: Stephen Finucane --- htdocs/css/style.css | 12 ++++++---- patchwork/templates/patchwork/submission.html | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/htdocs/css/style.css b/htdocs/css/style.css index f30988e0..a2a2e3c3 100644 --- a/htdocs/css/style.css +++ b/htdocs/css/style.css @@ -296,17 +296,19 @@ table.patch-meta tr th, table.patch-meta tr td { color: #f7977a; } -.comment .meta { +.submission-message .meta { + display: flex; + align-items: center; background: #f0f0f0; padding: 0.3em 0.5em; } -.comment .content { - border: 0; +.submission-message .message-date { + margin-left: 8px; } -.patch .content { - padding: 1em; +.submission-message .content { + border: 0; } .patch-pull-url { diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html index 66efa0b5..36b15d0e 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -257,14 +257,14 @@ {% else %}

Message

{% endif %} -
-
- {{ submission.submitter|personify:project }} - {{ submission.date }} UTC -
-
-{{ submission|commentsyntax }}
-
+
+
+ {{ submission.submitter|personify:project }} + {{ submission.date }} UTC +
+
+  {{ submission|commentsyntax }}
+  
{% for item in comments %} @@ -273,11 +273,12 @@ {% endif %} -
+
{{ item.submitter|personify:project }} - {{ item.date }} UTC | #{{ forloop.counter }} + {{ item.date }} UTC | + #{{ forloop.counter }} +
 {{ item|commentsyntax }}

From patchwork Fri Aug 20 04:50:23 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518911
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=SbnJ2hi4;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org
 [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTjR23jXz9sWl
	for ; Fri, 20 Aug 2021 14:50:51 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTjR1C2Tz3cSW
	for ; Fri, 20 Aug 2021 14:50:51 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=SbnJ2hi4;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::f4a; helo=mail-qv1-xf4a.google.com;
 envelope-from=3ndqfyqukczecviz619916z.x97avex2h9c563ded.9k6vwd.9c1@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=SbnJ2hi4; dkim-atps=neutral
Received: from mail-qv1-xf4a.google.com (mail-qv1-xf4a.google.com
 [IPv6:2607:f8b0:4864:20::f4a])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjC3wBZz30MC
 for ; Fri, 20 Aug 2021 14:50:39 +1000 (AEST)
Received: by mail-qv1-xf4a.google.com with SMTP id
 z8-20020a0ce9880000b02903528f1b338aso6110084qvn.6
 for ; Thu, 19 Aug 2021 21:50:38 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=tu98THFyvy8scUNoDMAkb1Gs6FqwhcfNw88/tLxaDAs=;
 b=SbnJ2hi4bYzOGXvcBKmfI6VfFOKUYhZ1bud8HXNhacOf8jkON5Y1Xu1iRWfkCuApXm
 epRqJ9E/BhOrQfLZPqam7bW/EoiQ+NLyPNegDunhKcDhsPUAW7AuPLwvOXZgsgBPe6rT
 pjviB7SP8LSowFjU8ozyMf2FWqAQQXk65veQdgajBafjACCOLjj1RWW8NLpDZ4kEcHAi
 yge82LoH7LVPPcQS3gMCXXfshBNli+YdLvWNpBjy0zQBIqoGQHcyX56fVzgo4ItTas58
 44DbG0NvDvyyYwG4UVotzVP+BSyKr2MqpNOs6WkkoLORPh4D3oHYSyjMFDMn8GB0JM7L
 61GA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=tu98THFyvy8scUNoDMAkb1Gs6FqwhcfNw88/tLxaDAs=;
 b=P/Pa2tyNssNLjk37OuxdUEufEXPqsXk7FZg5/+qznzVWnybFBIaqUqmIP6dvSMWNJH
 PL6ltlomFTAathP4OZVj8reYdnzxh+9nffbijMCw5THRu7hUGQO+CCAXYIyGiXslK496
 s2nJGP2Fl8HRNs3q/m8HadxYCxEszIzLko4zHz0BsKRWO++d4HeFIvzMhiu7ZiUTHR07
 YX0iqMmKFOWQ2O/OjEuZTxhp8sV5h8uF8cVcVOiTJ7FAqLvY7j5HUOqu8JjanM36ZBBL
 dmPnnxpg0mWAOmg1i3daNE7v5RvHp7DBxhvrH/J446c5WqXZt9CfmHxiZ5rnJ6e/Qh0r
 TWdw==
X-Gm-Message-State: AOAM531I72EThmKuhRUTMkh1cIwvyLUknFsYzRRcfq8nLxJ1PKvh461t
 zByFevMwbd6Y+pEK+eNOsMewugfBXmiTMu0NyIUG2f/MDEzdgqKhdJHDCzh3K89PfCWRCSy1hDu
 z1T9u6aF5dEpNshTe/ick92GEByMxGQDHFeFiI+G0zpxHHVPQ/OkuiOBT96fZT1EF
X-Google-Smtp-Source: 
 ABdhPJwWUj3nz8IO9n5SPgcUscW5Y50pvQneVtLki6WLf26P0GeMo3hjhe9EUcMsD4zDvk5b2Z3rrROx0g==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:a05:6214:291:: with SMTP id
 l17mr18463230qvv.50.1629435036301; Thu, 19 Aug 2021 21:50:36 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:23 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-3-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 2/9] api: change  parameter to  for cover
 comments endpoint
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Rename cover lookup parameter `pk` to `cover_id` for the cover comments
list endpoints to disambiguate from the lookup parameter `comment_id` in
upcoming patches which introduces the cover comments detail endpoint.
This doesn't affect the user-facing API.

Signed-off-by: Raxel Gutierrez 
Reviewed-by: Stephen Finucane 
---
 patchwork/api/comment.py            | 6 +++---
 patchwork/api/cover.py              | 2 +-
 patchwork/tests/api/test_comment.py | 4 ++--
 patchwork/urls.py                   | 2 +-
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/patchwork/api/comment.py b/patchwork/api/comment.py
index 0c578b44..5d7a77a1 100644
--- a/patchwork/api/comment.py
+++ b/patchwork/api/comment.py
@@ -83,14 +83,14 @@ class CoverCommentList(ListAPIView):
     search_fields = ('subject',)
     ordering_fields = ('id', 'subject', 'date', 'submitter')
     ordering = 'id'
-    lookup_url_kwarg = 'pk'
+    lookup_url_kwarg = 'cover_id'
 
     def get_queryset(self):
-        if not Cover.objects.filter(pk=self.kwargs['pk']).exists():
+        if not Cover.objects.filter(id=self.kwargs['cover_id']).exists():
             raise Http404
 
         return CoverComment.objects.filter(
-            cover=self.kwargs['pk']
+            cover=self.kwargs['cover_id']
         ).select_related('submitter')
 
 
diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py
index b4a3a8f7..c4355f6b 100644
--- a/patchwork/api/cover.py
+++ b/patchwork/api/cover.py
@@ -37,7 +37,7 @@ class CoverListSerializer(BaseHyperlinkedModelSerializer):
 
     def get_comments(self, cover):
         return self.context.get('request').build_absolute_uri(
-            reverse('api-cover-comment-list', kwargs={'pk': cover.id}))
+            reverse('api-cover-comment-list', kwargs={'cover_id': cover.id}))
 
     def to_representation(self, instance):
         # NOTE(stephenfin): This is here to ensure our API looks the same even
diff --git a/patchwork/tests/api/test_comment.py b/patchwork/tests/api/test_comment.py
index 59450d8a..53abf8f0 100644
--- a/patchwork/tests/api/test_comment.py
+++ b/patchwork/tests/api/test_comment.py
@@ -27,7 +27,7 @@ class TestCoverComments(utils.APITestCase):
         kwargs = {}
         if version:
             kwargs['version'] = version
-        kwargs['pk'] = cover.id
+        kwargs['cover_id'] = cover.id
 
         return reverse('api-cover-comment-list', kwargs=kwargs)
 
@@ -79,7 +79,7 @@ class TestCoverComments(utils.APITestCase):
     def test_list_invalid_cover(self):
         """Ensure we get a 404 for a non-existent cover letter."""
         resp = self.client.get(
-            reverse('api-cover-comment-list', kwargs={'pk': '99999'}))
+            reverse('api-cover-comment-list', kwargs={'cover_id': '99999'}))
         self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code)
 
 
diff --git a/patchwork/urls.py b/patchwork/urls.py
index 1e6c12ab..0180e76d 100644
--- a/patchwork/urls.py
+++ b/patchwork/urls.py
@@ -337,7 +337,7 @@ if settings.ENABLE_REST_API:
             name='api-patch-comment-list',
         ),
         path(
-            'covers//comments/',
+            'covers//comments/',
             api_comment_views.CoverCommentList.as_view(),
             name='api-cover-comment-list',
         ),

From patchwork Fri Aug 20 04:50:24 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518913
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=112.213.38.117; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=sILmjzsA;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTjh3D5wz9sWl
	for ; Fri, 20 Aug 2021 14:51:04 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTjh1lWbz3cVb
	for ; Fri, 20 Aug 2021 14:51:04 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=sILmjzsA;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::84a; helo=mail-qt1-x84a.google.com;
 envelope-from=3njqfyqukczmexk183bb381.zb9cxgz4jbe785fgf.bm8xyf.be3@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=sILmjzsA; dkim-atps=neutral
Received: from mail-qt1-x84a.google.com (mail-qt1-x84a.google.com
 [IPv6:2607:f8b0:4864:20::84a])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjF18zFz30MC
 for ; Fri, 20 Aug 2021 14:50:40 +1000 (AEST)
Received: by mail-qt1-x84a.google.com with SMTP id
 m8-20020a05622a0548b029028e6910f18aso4130131qtx.4
 for ; Thu, 19 Aug 2021 21:50:40 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=0funGMuP+0XP4E8LWGPD0UFJnoaJlhodnoLx+21O2NU=;
 b=sILmjzsADx8smGXr3/wZ3vCjkXIUdyRsQq4hWMO0tu/l6+Ueq94l2+3lIqkDP0fkON
 mrJXo+IWlqr/RrLAnSxFBxyLZpGxR3Ayr/kX36K33MXXw8IedAYUmzTDot0u83/rrCHD
 c4OX5SgL/EELgoDeLFEpPw/ERvceibV7cp0UrZCr5JZNXbFLEh1FrFWPwwbsj7FiNgYP
 Olk8bjaFejhq0q3oIij+K7haWol7XUPmPKSZSkOicFPG5o633D7uebih/B/q6gmTfNY/
 PLskF1arDSKc/ZyeTz8Xmf4JggkC3RrLDhTEryS3p7pcRjyu/8iz2rSlwQ4iG0NZj6No
 vS0w==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=0funGMuP+0XP4E8LWGPD0UFJnoaJlhodnoLx+21O2NU=;
 b=NYi79ukdynmld976hYr8+QYlhUK+Z1u/USLPZXL3j7HIaT22hJ2W0yOwh7mxBFw6pK
 obFjnJPTQLGFUx1AvV9TS6FAxSbeaV4E8O2Mkvny4jD0HDhAQVoFjDAba2OGmmR4W1ZN
 L5cfjcPlk7fmZYjerf5TmCWPR7ptHjVNRbqxDzCNK7hFgosOLBbd4RuGrlhGVMIlBvtR
 ptxACpzSlANa3mJ1h80qZcyOkQKWV07N5vrZ0ceH8qURPd7CZZcDDZ4VEGs699lU/HL4
 b2yvgGFbjf+7ovOnrEr6IVZEtPR3emEKPsCyVv467VIsmglQP0IGsC02vaq6PTf4HK2n
 4dIQ==
X-Gm-Message-State: AOAM533qU8XjFWimOu1TxU4mjfT3RYGyS/sU7uMlyoJnuPefSfdgLXTT
 ev1OiSl4kkL5KybaVHcaC0k7Ui2R2l9puqlfpg6WUDmUcs9GnNiMXnJxuAXQd8i2yg2fQBaUbe0
 FBcIOT6ln+QPkv3mjeLGQ6GnXiTU17ApysIAs9W8ve3kjULaxF9yVg5BKoGYgf7wy
X-Google-Smtp-Source: 
 ABdhPJxLoY/vz72/3gunUU86RCJazDsscgVOf7FeYdBsUBDnlXHgeoM2dLvb+4aC9i7HwBc7UDATPC2HHQ==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:a05:6214:f6c:: with SMTP id
 iy12mr18178020qvb.10.1629435038401; Thu, 19 Aug 2021 21:50:38 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:24 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-4-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 3/9] templatetags: add utils template filters and tags
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Add utils.py file to create template filters and tags that can be used
by most if not all objects in Patchwork. In particular, add a template
filter to get the plural verbose name of a model and add a template tag
that returns whether an object is editable by the current user. These
utilities will be used in an upcoming patch that adds the `addressed`
status label to patch and cover comments.

Signed-off-by: Raxel Gutierrez 
Reviewed-by: Stephen Finucane 
---
 patchwork/templatetags/utils.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 patchwork/templatetags/utils.py

diff --git a/patchwork/templatetags/utils.py b/patchwork/templatetags/utils.py
new file mode 100644
index 00000000..78c0aac8
--- /dev/null
+++ b/patchwork/templatetags/utils.py
@@ -0,0 +1,18 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2021 Google LLC
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from django import template
+
+register = template.Library()
+
+
+@register.filter
+def verbose_name_plural(obj):
+    return obj._meta.verbose_name_plural
+
+
+@register.simple_tag
+def is_editable(obj, user):
+    return obj.is_editable(user)

From patchwork Fri Aug 20 04:50:25 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518914
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=Wf6ZwGwM;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org
 [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTjn6HJ8z9sWl
	for ; Fri, 20 Aug 2021 14:51:09 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTjn58Gqz3cLw
	for ; Fri, 20 Aug 2021 14:51:09 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=Wf6ZwGwM;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::74a; helo=mail-qk1-x74a.google.com;
 envelope-from=3otqfyqukczyh0n4b6ee6b4.2ecf0j27mehab8iji.epb01i.eh6@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=Wf6ZwGwM; dkim-atps=neutral
Received: from mail-qk1-x74a.google.com (mail-qk1-x74a.google.com
 [IPv6:2607:f8b0:4864:20::74a])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjJ1Kh2z30MC
 for ; Fri, 20 Aug 2021 14:50:44 +1000 (AEST)
Received: by mail-qk1-x74a.google.com with SMTP id
 y185-20020a3764c20000b02903d2c78226ceso5704508qkb.6
 for ; Thu, 19 Aug 2021 21:50:43 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=1JiEoduWLNwfe4aFc7LehYqSDyxHe3skTSOkDEkB6Oo=;
 b=Wf6ZwGwMo6ayT8wdWCWlH1TuC3ZlMJl1ZsLjuhZGTL3TDjzZsphq65zkhUMA/Vw9W4
 71R6+npzbdgZLGQ2rOdWrRS2KocpFhpWh5KwVP/ySZ1wlozoubG7nGaQ5HWga17DILGO
 ToGKR4ArvyIUsHnjHwY2mIopAhWtck+J2Y3LetMIdHb1v9JuxBurm9ISQFSd3/tksntO
 G7aigilkJa8ZQc8wtZd5I/60Z9DWJPRjckhWS8kNIHBH7km79PqN98iqdalNwRyQmcvm
 F59PoMYQP9Hw32G2mzE0VOKHrtZz543ECwXKIzofd5Ccf1bKWqkEZQZJxItuOaAGq5Pk
 SplQ==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=1JiEoduWLNwfe4aFc7LehYqSDyxHe3skTSOkDEkB6Oo=;
 b=d+r8HBvDd0pJi0ICkPeYfyKjvMYahvj9Qxa/dY3sZ6BNctMdcp+K4s3xDbTR4TTg1V
 TkeYx16VbITaiAim/7hRsev9HPGIrrecX4PsciLjdebw6kMVopVOd2MDea82V5lGsXhn
 RHNRPg0QIxCilBaWjMPUBMutOaQ2u6AgRbjV/OHM7P5PdL3RLRC7jvOgS+r+f7fojiRv
 zdrH/K37yW8gKJfwiwa+TtmihCD7Xj4JQ0dGHT0s4DaPef9T0iLg0knQX5ywKIHojM5/
 EGHgUBUwq41SLw+rN6fviD+b0+BXTRwaqH+6AdLieYUDc5RYzPeXEVYWc0LBFt4O4jFx
 EtVQ==
X-Gm-Message-State: AOAM532dlUbvDQ+wSvYr9c7jqPEdgMSAMTb0LqxYjMQ/FmLx3+orkVkf
 teBIU2cHEOTSfK7brqz59qJh+GhxjNb2iqX+4QNT39kg2OE6mSxRjVGy4vNIfPeWpHIN1/wk2QV
 0JaOmOvEL/EdDs3a3soV/IQVSwQF+omQJIfJuAfOj+NSYXvvjq2o6caRW5obMjl1M
X-Google-Smtp-Source: 
 ABdhPJxQ6C7xGVDJc3JRb9suwATmz9nPDR1XwxB5E9ceoWEJowRrWr73rkPFn3lKcN9qvLHL8d9ydKYDjg==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:a05:6214:324:: with SMTP id
 j4mr18231898qvu.22.1629435041094; Thu, 19 Aug 2021 21:50:41 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:25 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-5-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 4/9] models: add addressed field
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Currently, there is no state or status associated with comments. In
particular, knowing whether a comment on a patch or cover letter is
addressed or not is useful for transparency and accountability in the
patch review and contribution process. This patch is backend setup for
tracking the state of patch and cover comments.

Add `addressed` boolean field to patch and cover comments to be able to
distinguish between unaddressed and addressed comments in the
patch-detail page.

Signed-off-by: Raxel Gutierrez 
Reviewed-by: Daniel Axtens 
---
 .../migrations/0045_auto_20210817_0136.py     | 23 +++++++++++++++++++
 patchwork/models.py                           |  2 ++
 2 files changed, 25 insertions(+)
 create mode 100644 patchwork/migrations/0045_auto_20210817_0136.py

diff --git a/patchwork/migrations/0045_auto_20210817_0136.py b/patchwork/migrations/0045_auto_20210817_0136.py
new file mode 100644
index 00000000..ed3527bc
--- /dev/null
+++ b/patchwork/migrations/0045_auto_20210817_0136.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.12 on 2021-08-17 01:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('patchwork', '0044_add_project_linkname_validation'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='covercomment',
+            name='addressed',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='patchcomment',
+            name='addressed',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index 00273da9..90e34815 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -657,6 +657,7 @@ class CoverComment(EmailMixin, models.Model):
         related_query_name='comment',
         on_delete=models.CASCADE,
     )
+    addressed = models.BooleanField(default=False)
 
     @property
     def list_archive_url(self):
@@ -693,6 +694,7 @@ class PatchComment(EmailMixin, models.Model):
         related_query_name='comment',
         on_delete=models.CASCADE,
     )
+    addressed = models.BooleanField(default=False)
 
     @property
     def list_archive_url(self):

From patchwork Fri Aug 20 04:50:26 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518915
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=112.213.38.117; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=jOovgmU/;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTjv1Gshz9sWl
	for ; Fri, 20 Aug 2021 14:51:15 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTjv0D0lz3cPy
	for ; Fri, 20 Aug 2021 14:51:15 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=jOovgmU/;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::84a; helo=mail-qt1-x84a.google.com;
 envelope-from=3ojqfyqukczci1o5c7ff7c5.3fdg1k38nfibc9jkj.fqc12j.fi7@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=jOovgmU/; dkim-atps=neutral
Received: from mail-qt1-x84a.google.com (mail-qt1-x84a.google.com
 [IPv6:2607:f8b0:4864:20::84a])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjJ3JTNz30MC
 for ; Fri, 20 Aug 2021 14:50:44 +1000 (AEST)
Received: by mail-qt1-x84a.google.com with SMTP id
 98-20020aed316b000000b00298da0dd56bso4111654qtg.13
 for ; Thu, 19 Aug 2021 21:50:44 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=RzmLzaU3bb+iK2yb7ekv0aNwNuD1qZX0zEa0KX6g/8s=;
 b=jOovgmU/KnI2tVkbw+MRAh4bJkSesUErKDMtevp3Up3Kft7C/zb22EHZZhaKFP1/X6
 LFhYsmTkWxji+CU7/xkYq0CeIOrUwzjH1VLOzWy0rfof6Mfr0CZ745qddVQ8KRKfUNkO
 Ne46WtFRNoF41EpYAgCI1GGtrYSJodK9SLzm5ZwiuNTCb+xbnygd4rm8pIkGPXkVOFPq
 qm9Q897A1I6nPwgQzoAG6jXkCG5nvHDIxobPOpDcfbMh3iT1SjffG5gtD3Ci0EOIZlpL
 cQKl87teST33hhoPk/kMYLYGYP65efntevZ5MsCi7ZoKkS+Pb0eB0VMMmXiBozqO+jA2
 h9NA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=RzmLzaU3bb+iK2yb7ekv0aNwNuD1qZX0zEa0KX6g/8s=;
 b=ewqzxvv7l6l2EyveuRWNuWcRKz6QCIG8S2Yy0EH0q40F+HOmCFmfRi9s612TULFRyp
 tIM5zuT4j4aByP0MpJ8x1SaDdVQ5PB4RXlhiW+HrMugaZ7i1aiXp91FdKKutUWWO9/CA
 hLGccyOUs/HKVOeCUc/RIQy9CPqE1gqevujT0Ga2XVE93q5jeiGoxqtm5gR2OqMb5G4y
 f+WAlR+nQxX/BJkG7J5R7d4yHLsHdUal3XbduPCk30Vd9d813/FKFF/qSq26CpVSo0QP
 nYiio9TyT8TIKKwhPBM6e8SHchkJivEkp2A5EOkEs0Aqd1LUMctnUhD5zGJBRjFhLO7r
 oWaQ==
X-Gm-Message-State: AOAM531Atu6DHOt6DFEUtp/jfMxW4YgVmxUFJ5kBjed7WD77shbv7+SG
 kJAb3VYaKri5wXk1YtXDt4iD47lz51o3RxCsac8nO1v4ZNhraK6p7B4B7FTz7hYr7DSU9q6jiFB
 tLzpkMTyi+goxqhRmJqIyVnYtUUbIOvUQz2v99cXtLUQNRkMqK6HtSmTyGo2hnuLi
X-Google-Smtp-Source: 
 ABdhPJzP3R7F0MrbmGr3FItQ4nydQUoltEZITDLdw/eo+nLDmQh5iUcC6AfphDH/RNfPjPSgsUQdq+iSPA==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:a05:6214:27ea:: with SMTP id
 jt10mr18481277qvb.39.1629435042297; Thu, 19 Aug 2021 21:50:42 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:26 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-6-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 5/9] models: change edit permissions for comments
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Change patch comments' edit permissions to match that of the patch
associated with the comment (i.e. patch author, project maintainers, and
delegate) and add permissions for both patch and cover comment authors
to be able to change the `addressed` status of comments as well. For
cover comments, add permissions to edit for cover submitter and project
maintainers.

Signed-off-by: Raxel Gutierrez 
---
 patchwork/models.py | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/patchwork/models.py b/patchwork/models.py
index 90e34815..58e4c51e 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -675,6 +675,20 @@ class CoverComment(EmailMixin, models.Model):
         return reverse('comment-redirect', kwargs={'comment_id': self.id})
 
     def is_editable(self, user):
+        if not user.is_authenticated:
+            return False
+
+        # user submitted comment
+        if user == self.submitter.user:
+            return True
+
+        # user submitted cover letter
+        if user == self.cover.submitter.user:
+            return True
+
+        # user is project maintainer
+        if self.cover.project.is_editable(user):
+            return True
         return False
 
     class Meta:
@@ -720,7 +734,9 @@ class PatchComment(EmailMixin, models.Model):
         self.patch.refresh_tag_counts()
 
     def is_editable(self, user):
-        return False
+        if user == self.submitter.user:
+            return True
+        return self.patch.is_editable(user)
 
     class Meta:
         ordering = ['date']

From patchwork Fri Aug 20 04:50:27 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518916
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=112.213.38.117; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=WKi/0dFV;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTk06q4Tz9sWl
	for ; Fri, 20 Aug 2021 14:51:20 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTk05NHlz3cSV
	for ; Fri, 20 Aug 2021 14:51:20 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=WKi/0dFV;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::f49; helo=mail-qv1-xf49.google.com;
 envelope-from=3ozqfyqukczgj2p6d8gg8d6.4geh2l49ogjcdaklk.grd23k.gj8@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=WKi/0dFV; dkim-atps=neutral
Received: from mail-qv1-xf49.google.com (mail-qv1-xf49.google.com
 [IPv6:2607:f8b0:4864:20::f49])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjL0WrVz3bnK
 for ; Fri, 20 Aug 2021 14:50:45 +1000 (AEST)
Received: by mail-qv1-xf49.google.com with SMTP id
 l3-20020a056214104300b00366988901acso599640qvr.2
 for ; Thu, 19 Aug 2021 21:50:45 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=oQc2YwSbcnOvYINFwZttcSsqlSdKkfKzP/xXucOfKmU=;
 b=WKi/0dFV2w4xs3e4ptrBywG3qefiQ2hhqId0SdIYc3y7+TbYNmBd0b5WU46i2RROO3
 tUCGaYspIQ8ju1G7wodq8T7zj1J9tXFDK9S1BBL0yQFuasbBE8MlVPnnYh1csdUjQBPi
 2VACT4OhcF9nB91xprvudaX1pbO4BK5jldjZg5OmwJ8wHRmyZva+6Bu81ox6NfGWDxwF
 5+ANUvoIVqbKRAtPDl+rvAbcs/kDABP4pXUDbsCNDxWN3tPdzA4SfFx0TlvfaXWZGh6j
 dt5FZ94trvoms6sIO91QxJMV34C786XlFPgodQp9jRWcqochKknI5z3WOemtb8wLlGD+
 6nnQ==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=oQc2YwSbcnOvYINFwZttcSsqlSdKkfKzP/xXucOfKmU=;
 b=prJk7maGRzQwbSKylhS+XqRx/SQm2R+TmUScVeUf6EEv/8n/GIbaRaYIdI0qoMFfTc
 JAfz4qNIyKXq2HPV9SHnNG5QtpzfSiWG/OI/vS1RsW1tJ3r3qy8bsHEzLKsC2T3QCBGr
 T2Ia73ClgfUCENgmMxAMLFZniXYrFUB3u1znNauJR0aub/FcgPLUlRRcyZ5gb7BVWn9S
 qRI2Ul+n9lZ3SO0lXZWkvAaBCzgsFnSBRNchnBQuQHBdalOpgh5/wV77cKNWhO7Qb1m9
 AgmGCKJjVmDdrONmJko3RHWq0WaWt/jzNngdzySoEkl7yB3Z4+BdUY+ken+rGrmK/WTB
 XDcw==
X-Gm-Message-State: AOAM533kfgwIm9X9UYk+94FyLWL2vlYR/GLRaeKkPsf15atZH8/ZzkNW
 ZLeC1hRuc3DuNtVpFIQirv4SHtQKB6AA9rtFtK4PvZEcJYId0Ry8mp1rQ6R/n3wovjgeYi5aMNz
 gYGyGHe5rh5WshgdoHnmoVlNMaw4g+AjIAu1U1oNy/CPG7EXi6+OfviXyiGbSsN/m
X-Google-Smtp-Source: 
 ABdhPJzENUurNaJayvZHW/c0erTmhDQajnNaVZMIdenmDgB5TmoSiQyN/q8UJjWdXemQ+uQKeBfZfC2RQg==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:a0c:ecc4:: with SMTP id
 o4mr18077249qvq.18.1629435043457;
 Thu, 19 Aug 2021 21:50:43 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:27 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-7-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 6/9] api: add comments detail endpoint
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Add new endpoint for patch and cover comments at api/.../comments/.
This involves updating the API version to v1.3 to reflect the new
endpoints as a minor change, following the usual semantic versioning
convention.

The endpoint will make it possible to use the REST API to update the new
`addressed` field for individual patch and cover comments with JavaScript
on the client side. In the process of these changes, clean up the 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).

The tests cover retrieval and update requests and also handle calls from
the various API versions. Also, they cover permissions for update
requests and handle invalid update values for the new `addressed` field.

Signed-off-by: Raxel Gutierrez 
---
 docs/api/schemas/generate-schemas.py |   4 +-
 docs/api/schemas/patchwork.j2        | 165 ++++++++++++
 patchwork/api/base.py                |  24 +-
 patchwork/api/check.py               |  20 +-
 patchwork/api/comment.py             | 146 ++++++++--
 patchwork/tests/api/test_comment.py  | 388 ++++++++++++++++++++++++---
 patchwork/urls.py                    |  20 +-
 7 files changed, 682 insertions(+), 85 deletions(-)

diff --git a/docs/api/schemas/generate-schemas.py b/docs/api/schemas/generate-schemas.py
index a0c1e45f..3a436a16 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/patchwork.j2 b/docs/api/schemas/patchwork.j2
index af20743d..3b4ad2f6 100644
--- a/docs/api/schemas/patchwork.j2
+++ b/docs/api/schemas/patchwork.j2
@@ -324,6 +324,74 @@ paths:
                 $ref: '#/components/schemas/Error'
       tags:
         - comments
+{% if version >= (1, 3) %}
+  /api/{{ version_url }}covers/{cover_id}/comments/{comment_id}/:
+    parameters:
+      - in: path
+        name: cover_id
+        description: A unique integer value identifying the parent cover.
+        required: true
+        schema:
+          title: Cover 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 cover comment.
+      operationId: cover_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 cover comment (partial).
+      operationId: cover_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 }}events/:
     get:
       description: List events.
@@ -656,6 +724,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 +1413,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 +1730,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 +2814,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/patchwork/api/base.py b/patchwork/api/base.py
index 89a43114..a98644ee 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, PatchComment
+    and CoverComment 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 a6bf5f8c..2049d2f9 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 5d7a77a1..334016d7 100644
--- a/patchwork/api/comment.py
+++ b/patchwork/api/comment.py
@@ -4,13 +4,19 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
 import email.parser
+import rest_framework
 
+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
@@ -18,6 +24,24 @@ from patchwork.models import Patch
 from patchwork.models import PatchComment
 
 
+DRF_VERSION = tuple(int(x) for x in rest_framework.__version__.split('.'))
+
+
+if DRF_VERSION > (3, 11):
+    class CurrentCoverDefault(object):
+        requires_context = True
+
+        def __call__(self, serializer_field):
+            return serializer_field.context['request'].cover
+else:
+    class CurrentCoverDefault(object):
+        def set_context(self, serializer_field):
+            self.patch = serializer_field.context['request'].cover
+
+        def __call__(self):
+            return self.cover
+
+
 class BaseCommentListSerializer(BaseHyperlinkedModelSerializer):
 
     web_url = SerializerMethodField()
@@ -49,65 +73,133 @@ class BaseCommentListSerializer(BaseHyperlinkedModelSerializer):
 
     class Meta:
         fields = ('id', 'web_url', 'msgid', 'list_archive_url', 'date',
-                  'subject', 'submitter', 'content', 'headers')
-        read_only_fields = fields
+                  'subject', 'submitter', 'content', 'headers', 'addressed')
+        read_only_fields = ('id', 'web_url', 'msgid', 'list_archive_url',
+                            'date', 'subject', 'submitter', 'content',
+                            'headers')
         versioned_fields = {
             '1.1': ('web_url', ),
             '1.2': ('list_archive_url',),
+            '1.3': ('addressed',),
         }
 
 
-class CoverCommentListSerializer(BaseCommentListSerializer):
+class CoverCommentSerializer(BaseCommentListSerializer):
+
+    cover = HiddenField(default=CurrentCoverDefault())
 
     class Meta:
         model = CoverComment
-        fields = BaseCommentListSerializer.Meta.fields
-        read_only_fields = fields
+        fields = BaseCommentListSerializer.Meta.fields + (
+            'cover', 'addressed')
+        read_only_fields = BaseCommentListSerializer.Meta.read_only_fields + (
+            'cover', )
         versioned_fields = BaseCommentListSerializer.Meta.versioned_fields
+        extra_kwargs = {
+            'url': {'view_name': 'api-cover-comment-detail'}
+        }
+
 
+class CoverCommentMixin(object):
+
+    permission_classes = (PatchworkPermission,)
+    serializer_class = CoverCommentSerializer
 
-class PatchCommentListSerializer(BaseCommentListSerializer):
+    def get_object(self):
+        queryset = self.filter_queryset(self.get_queryset())
+        comment_id = self.kwargs['comment_id']
+        obj = get_object_or_404(queryset, id=int(comment_id))
+        self.check_object_permissions(self.request, obj)
+        return obj
+
+    def get_queryset(self):
+        cover_id = self.kwargs['cover_id']
+        if not Cover.objects.filter(id=cover_id).exists():
+            raise Http404
+
+        return CoverComment.objects.filter(
+            cover=cover_id
+        ).select_related('submitter')
+
+
+class PatchCommentSerializer(BaseCommentListSerializer):
+
+    patch = HiddenField(default=CurrentPatchDefault())
 
     class Meta:
         model = PatchComment
-        fields = BaseCommentListSerializer.Meta.fields
-        read_only_fields = fields
+        fields = BaseCommentListSerializer.Meta.fields + ('patch', )
+        read_only_fields = BaseCommentListSerializer.Meta.read_only_fields + (
+            'patch', )
         versioned_fields = BaseCommentListSerializer.Meta.versioned_fields
+        extra_kwargs = {
+            'url': {'view_name': 'api-patch-comment-detail'}
+        }
 
 
-class CoverCommentList(ListAPIView):
-    """List cover comments"""
+class PatchCommentMixin(object):
 
     permission_classes = (PatchworkPermission,)
-    serializer_class = CoverCommentListSerializer
+    serializer_class = PatchCommentSerializer
+
+    def get_object(self):
+        queryset = self.filter_queryset(self.get_queryset())
+        comment_id = self.kwargs['comment_id']
+        obj = get_object_or_404(queryset, id=int(comment_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(CoverCommentMixin, ListAPIView):
+    """List cover comments"""
+
     search_fields = ('subject',)
     ordering_fields = ('id', 'subject', 'date', 'submitter')
     ordering = 'id'
     lookup_url_kwarg = 'cover_id'
 
-    def get_queryset(self):
-        if not Cover.objects.filter(id=self.kwargs['cover_id']).exists():
-            raise Http404
 
-        return CoverComment.objects.filter(
-            cover=self.kwargs['cover_id']
-        ).select_related('submitter')
+class CoverCommentDetail(CoverCommentMixin, MultipleFieldLookupMixin,
+                         RetrieveUpdateAPIView):
+    """
+    get:
+    Show a cover comment.
+    patch:
+    Update a cover comment.
+    put:
+    Update a cover comment.
+    """
+    lookup_url_kwargs = ('cover_id', 'comment_id')
+    lookup_fields = ('cover_id', 'id')
 
 
-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 53abf8f0..033a1416 100644
--- a/patchwork/tests/api/test_comment.py
+++ b/patchwork/tests/api/test_comment.py
@@ -9,11 +9,17 @@ from django.conf import settings
 from django.urls import NoReverseMatch
 from django.urls import reverse
 
+from patchwork.models import PatchComment
+from patchwork.models import CoverComment
 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:
@@ -23,18 +29,26 @@ if settings.ENABLE_REST_API:
 @unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
 class TestCoverComments(utils.APITestCase):
     @staticmethod
-    def api_url(cover, version=None):
-        kwargs = {}
+    def api_url(cover, version=None, item=None):
+        kwargs = {'cover_id': cover.id}
         if version:
             kwargs['version'] = version
-        kwargs['cover_id'] = cover.id
+        if item is None:
+            return reverse('api-cover-comment-list', kwargs=kwargs)
+        kwargs['comment_id'] = item.id
+        return reverse('api-cover-comment-detail', kwargs=kwargs)
 
-        return reverse('api-cover-comment-list', kwargs=kwargs)
+    def setUp(self):
+        super(TestCoverComments, self).setUp()
+        self.project = create_project()
+        self.user = create_maintainer(self.project)
+        self.cover = create_cover(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):
@@ -47,10 +61,9 @@ class TestCoverComments(utils.APITestCase):
     @utils.store_samples('cover-comment-list')
     def test_list(self):
         """List cover letter comments."""
-        cover = create_cover()
-        comment = create_cover_comment(cover=cover)
+        comment = create_cover_comment(cover=self.cover)
 
-        resp = self.client.get(self.api_url(cover))
+        resp = self.client.get(self.api_url(self.cover))
         self.assertEqual(status.HTTP_200_OK, resp.status_code)
         self.assertEqual(1, len(resp.data))
         self.assertSerialized(comment, resp.data[0])
@@ -58,23 +71,21 @@ class TestCoverComments(utils.APITestCase):
 
     def test_list_version_1_1(self):
         """List cover letter comments using API v1.1."""
-        cover = create_cover()
-        comment = create_cover_comment(cover=cover)
+        create_cover_comment(cover=self.cover)
 
-        resp = self.client.get(self.api_url(cover, version='1.1'))
+        resp = self.client.get(self.api_url(self.cover, 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])
+        self.assertNotIn('addressed', resp.data[0])
 
     def test_list_version_1_0(self):
-        """List cover letter comments using API v1.0."""
-        cover = create_cover()
-        create_cover_comment(cover=cover)
+        """List cover letter comments using API v1.0.
 
-        # check we can't access comments using the old version of the API
+        Ensure we can't access cover comments using the old version of the API.
+        """
         with self.assertRaises(NoReverseMatch):
-            self.client.get(self.api_url(cover, version='1.0'))
+            self.client.get(self.api_url(self.cover, version='1.0'))
 
     def test_list_invalid_cover(self):
         """Ensure we get a 404 for a non-existent cover letter."""
@@ -82,38 +93,193 @@ class TestCoverComments(utils.APITestCase):
             reverse('api-cover-comment-list', kwargs={'cover_id': '99999'}))
         self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code)
 
+    @utils.store_samples('cover-comment-detail')
+    def test_detail(self):
+        """Show a cover letter comment."""
+        comment = create_cover_comment(cover=self.cover)
+
+        resp = self.client.get(self.api_url(self.cover, 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 cover letter comment using API v1.3."""
+        comment = create_cover_comment(cover=self.cover)
+
+        resp = self.client.get(
+            self.api_url(self.cover, 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 cover letter comment using API v1.2."""
+        comment = create_cover_comment(cover=self.cover)
+
+        with self.assertRaises(NoReverseMatch):
+            self.client.get(
+                self.api_url(self.cover, version='1.2', item=comment))
+
+    def test_detail_version_1_1(self):
+        """Show a cover letter comment using API v1.1."""
+        comment = create_cover_comment(cover=self.cover)
+
+        with self.assertRaises(NoReverseMatch):
+            self.client.get(
+                self.api_url(self.cover, version='1.1', item=comment))
+
+    def test_detail_version_1_0(self):
+        """Show a cover letter comment using API v1.0."""
+        comment = create_cover_comment(cover=self.cover)
+
+        with self.assertRaises(NoReverseMatch):
+            self.client.get(
+                self.api_url(self.cover, version='1.0', item=comment))
+
+    @utils.store_samples('cover-comment-detail-error-not-found')
+    def test_detail_invalid_cover(self):
+        """Ensure we handle non-existent cover letters."""
+        comment = create_cover_comment()
+        resp = self.client.get(
+            reverse('api-cover-comment-detail', kwargs={
+                'cover_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)
+        cover = kwargs.get('cover', self.cover)
+        comment = create_cover_comment(submitter=submitter, cover=cover)
+
+        if kwargs.get('authenticate', True):
+            self.client.force_authenticate(user=person.user)
+        return self.client.patch(
+            self.api_url(cover, item=comment),
+            {'addressed': kwargs.get('addressed', True)},
+            validate_request=kwargs.get('validate_request', True)
+        )
+
+    @utils.store_samples('cover-comment-detail-update-authorized')
+    def test_update_authorized(self):
+        """Update an existing cover letter comment as an authorized user.
+
+        To be authorized users must meet at least one of the following:
+        - project maintainer, cover letter submitter, or cover letter
+          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, CoverComment.objects.all().count())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertTrue(resp.data['addressed'])
+
+        # Update as cover letter submitter
+        person = create_person(name='cover-submitter', user=create_user())
+        cover = create_cover(submitter=person)
+        resp = self._test_update(person=person, cover=cover)
+        self.assertEqual(2, CoverComment.objects.all().count())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertTrue(resp.data['addressed'])
+
+        # Update as cover letter comment submitter
+        person = create_person(name='comment-submitter', user=create_user())
+        cover = create_cover()
+        resp = self._test_update(person=person, cover=cover, submitter=person)
+        self.assertEqual(3, CoverComment.objects.all().count())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertTrue(resp.data['addressed'])
+
+    @utils.store_samples('cover-comment-detail-update-not-authorized')
+    def test_update_not_authorized(self):
+        """Update an existing cover letter comment when not signed in and
+           not authorized.
+
+        To be authorized users must meet at least one of the following:
+        - project maintainer, cover letter submitter, or cover letter
+          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('cover-comment-detail-update-error-bad-request')
+    def test_update_invalid_addressed(self):
+        """Update an existing cover letter comment using invalid values.
+
+        Ensure we handle invalid cover letter comment addressed values.
+        """
+        person = create_person(name='cover-submitter', user=create_user())
+        cover = create_cover(submitter=person)
+        resp = self._test_update(person=person,
+                                 cover=cover,
+                                 addressed='not-valid',
+                                 validate_request=False)
+        self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
+        self.assertFalse(
+            getattr(CoverComment.objects.all().first(), 'addressed')
+        )
+
+    def test_create_delete(self):
+        """Ensure creates and deletes aren't allowed"""
+        comment = create_cover_comment(cover=self.cover)
+        self.user.is_superuser = True
+        self.user.save()
+        self.client.force_authenticate(user=self.user)
+
+        resp = self.client.post(self.api_url(self.cover, item=comment))
+        self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+        resp = self.client.delete(self.api_url(self.cover, item=comment))
+        self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
 
 @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 +287,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)
+        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])
+        self.assertNotIn('addressed', 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 0180e76d..b3f40cbf 100644
--- a/patchwork/urls.py
+++ b/patchwork/urls.py
@@ -343,12 +343,28 @@ if settings.ENABLE_REST_API:
         ),
     ]
 
+    api_1_3_patterns = [
+        path(
+            'patches//comments//',
+            api_comment_views.PatchCommentDetail.as_view(),
+            name='api-patch-comment-detail',
+        ),
+        path(
+            'covers//comments//',
+            api_comment_views.CoverCommentDetail.as_view(),
+            name='api-cover-comment-detail',
+        ),
+    ]
+
     urlpatterns += [
         re_path(
-            r'^api/(?:(?P(1.0|1.1|1.2))/)?', include(api_patterns)
+            r'^api/(?:(?P(1.0|1.1|1.2|1.3))/)?', include(api_patterns)
+        ),
+        re_path(
+            r'^api/(?:(?P(1.1|1.2|1.3))/)?', include(api_1_1_patterns)
         ),
         re_path(
-            r'^api/(?:(?P(1.1|1.2))/)?', include(api_1_1_patterns)
+            r'^api/(?:(?P(1.3))/)?', include(api_1_3_patterns)
         ),
         # token change
         path(

From patchwork Fri Aug 20 04:50:28 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518917
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=iMOZkWuo;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org
 [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTk71bkgz9sWl
	for ; Fri, 20 Aug 2021 14:51:27 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTk707Xmz3cQb
	for ; Fri, 20 Aug 2021 14:51:27 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=iMOZkWuo;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::f49; helo=mail-qv1-xf49.google.com;
 envelope-from=3pdqfyqukczkk3q7e9hh9e7.5hfi3m5aphkdeblml.hse34l.hk9@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=iMOZkWuo; dkim-atps=neutral
Received: from mail-qv1-xf49.google.com (mail-qv1-xf49.google.com
 [IPv6:2607:f8b0:4864:20::f49])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjL5wjZz3cJ0
 for ; Fri, 20 Aug 2021 14:50:46 +1000 (AEST)
Received: by mail-qv1-xf49.google.com with SMTP id
 q13-20020a0ce9cd000000b003608f06491fso6095113qvo.18
 for ; Thu, 19 Aug 2021 21:50:46 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=GbsI7nvEXWE/t46wORMgABwcy2hQ2+DdLEzFZoBfWj4=;
 b=iMOZkWuoDoinQyysJPyIl6COwdKoeA1CP6eOxh7aClSJNsJzQrLe7iIvwTrbFphbPa
 DzVH90v7Lb/tQNxjJ3wdZKtkt5NiMMBKfdE4jiMJQ5NhaQRqhujn2fHSnBgxJJC8PqND
 2ANry/iWaqrTwgsVivJbKuuqRW5uEifHsiT2juu0u+E6vBO1WXw7QfSjEz61XxZeViJH
 V63tlZxysYP9MCts7BWJRwittf7R94ybs6lDbn9tGqy26i8EUO005MAhXPJ9MBv+zX5+
 QmrL4xCIXiaha2eOuDHuYMUiI7b/E81iJVmoDvN/jB1M6/fBHL6CU2f5IDj8i7WRVjvZ
 1hjA==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=GbsI7nvEXWE/t46wORMgABwcy2hQ2+DdLEzFZoBfWj4=;
 b=T5RjFTDwA48530yV0wKdRZXPXitVxAJCgU9CUKef9xaLPySPe1TbqQ2IPZf7uHPKr2
 QP4o+Uy8nq2BW72j8zT7k5XIhr3THDhkcDrtEZxr+HIRW60tGEr1/twLTTLs9W2iLnK/
 WesSPj2xqOpLs7vMQEJXAhp6npOmxAbDlVAjoIl4m7U0qLjxoIdjSsKiH8xG/tiSwA5r
 OqEnWOfnXcYNUmJ4ETSi9WhK37NhsXCeZ8hmG4SxQlJwVJzo7E5a+LacYYRcxUosuyJz
 F/qu60E7btZqxJgxx+57Fw2s004y7xJUt53oLAZJCfKDp5+Lnzobup32yqvxNY8FNUZD
 MXFw==
X-Gm-Message-State: AOAM532tM5XYvC2R1DBnvSG2rIcyB6JwjGAlEOJrYMNB3JwgHCxSa51N
 qG6Kc6ixr3dL9UxMX6LdyUFwC41e7n2UIYaMj8M2eVlvTRBdFIetjM8lyzowv7KYFz86CydQusP
 3Gx0mzVYCzuU2HCDnqKOqUNXh7Oqt+JzryJgDjzdaeuIiBB1bmx7Hv4fmKraUs8MW
X-Google-Smtp-Source: 
 ABdhPJwmA2VRBhwLkGE5+2h5DwRMhReNXVj0EoCDNpY9JREWNZ5Bby6GmTXH3gL2bKOMTODdsNLfJJy47w==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:a05:6214:aa8:: with SMTP id
 ew8mr18213572qvb.43.1629435044705; Thu, 19 Aug 2021 21:50:44 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:28 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-8-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 7/9] api: add auto-generated OpenAPI schema files
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Signed-off-by: Raxel Gutierrez 
---
 docs/api/schemas/latest/patchwork.yaml |  159 +-
 docs/api/schemas/v1.3/patchwork.yaml   | 2770 ++++++++++++++++++++++++
 2 files changed, 2928 insertions(+), 1 deletion(-)
 create mode 100644 docs/api/schemas/v1.3/patchwork.yaml

diff --git a/docs/api/schemas/latest/patchwork.yaml b/docs/api/schemas/latest/patchwork.yaml
index a8910a7c..e3bff990 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:
@@ -317,6 +317,72 @@ paths:
                 $ref: '#/components/schemas/Error'
       tags:
         - comments
+  /api/covers/{cover_id}/comments/{comment_id}/:
+    parameters:
+      - in: path
+        name: cover_id
+        description: A unique integer value identifying the parent cover.
+        required: true
+        schema:
+          title: Cover 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 cover comment.
+      operationId: cover_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 cover comment (partial).
+      operationId: cover_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/events/:
     get:
       description: List events.
@@ -635,6 +701,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 +1374,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 +1666,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 +1859,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 +2704,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/v1.3/patchwork.yaml b/docs/api/schemas/v1.3/patchwork.yaml
new file mode 100644
index 00000000..6cbba646
--- /dev/null
+++ b/docs/api/schemas/v1.3/patchwork.yaml
@@ -0,0 +1,2770 @@
+# 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/covers/{cover_id}/comments/{comment_id}/:
+    parameters:
+      - in: path
+        name: cover_id
+        description: A unique integer value identifying the parent cover.
+        required: true
+        schema:
+          title: Cover 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 cover comment.
+      operationId: cover_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 cover comment (partial).
+      operationId: cover_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/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

From patchwork Fri Aug 20 04:50:29 2021
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
X-Patchwork-Submitter: Raxel Gutierrez 
X-Patchwork-Id: 1518918
Return-Path: 
 
X-Original-To: incoming@patchwork.ozlabs.org
Delivered-To: patchwork-incoming@bilbo.ozlabs.org
Authentication-Results: ozlabs.org;
 spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org
 (client-ip=112.213.38.117; helo=lists.ozlabs.org;
 envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;
 receiver=)
Authentication-Results: ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=Z5qiAr9i;
	dkim-atps=neutral
Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (4096 bits))
	(No client certificate requested)
	by ozlabs.org (Postfix) with ESMTPS id 4GrTkG5Nhcz9sWl
	for ; Fri, 20 Aug 2021 14:51:34 +1000 (AEST)
Received: from boromir.ozlabs.org (localhost [IPv6:::1])
	by lists.ozlabs.org (Postfix) with ESMTP id 4GrTkG3tZmz3cjX
	for ; Fri, 20 Aug 2021 14:51:34 +1000 (AEST)
Authentication-Results: lists.ozlabs.org;
	dkim=fail reason="signature verification failed" (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=Z5qiAr9i;
	dkim-atps=neutral
X-Original-To: patchwork@lists.ozlabs.org
Delivered-To: patchwork@lists.ozlabs.org
Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized)
 smtp.mailfrom=flex--raxel.bounces.google.com
 (client-ip=2607:f8b0:4864:20::74a; helo=mail-qk1-x74a.google.com;
 envelope-from=3pjqfyqukczsm5s9gbjjbg9.7jhk5o7crjmfgdnon.jug56n.jmb@flex--raxel.bounces.google.com;
 receiver=)
Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key;
 unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256
 header.s=20161025 header.b=Z5qiAr9i; dkim-atps=neutral
Received: from mail-qk1-x74a.google.com (mail-qk1-x74a.google.com
 [IPv6:2607:f8b0:4864:20::74a])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest
 SHA256)
 (No client certificate requested)
 by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjN3SyYz3bmn
 for ; Fri, 20 Aug 2021 14:50:48 +1000 (AEST)
Received: by mail-qk1-x74a.google.com with SMTP id
 q13-20020a05620a038d00b003d38f784161so5714761qkm.8
 for ; Thu, 19 Aug 2021 21:50:48 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
 s=20161025;
 h=date:in-reply-to:message-id:mime-version:references:subject:from:to
 :cc; bh=msKDKUmtDWKJmYvE8CxOLhiyS9H8DVoQQreyfB+ekYk=;
 b=Z5qiAr9iuvao0h+vfA3eyf/RKvDmBeyiLqG+JQK1yiO5PSF3mTfMFgntOJF8D8xdnK
 //3OqB6sxEqanfF+TmqwLDdXoCy3rw6zEP6gS7S9EANWMw/bFq+X4Pqnk34bkROuAkih
 S+Rz2ebDqojyvHsX3DOhKX/ap+h35cO8gS+1pxiPUGLXneCTcdJohTemr7qA9Z+WH7Jz
 +NLduhhJD9dpEkNUlNCtvhkeKISkZiY0byz69vfFqzVu9pLFIFWkbUGLXgIlli1ChKAO
 hD9MdMfBhDLPKbPADvuCIZcT0T9gH5Ib1M38MQqF2tPFqLgd2SXLS6iOLeRQCX2Yr1Cx
 Eqew==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20161025;
 h=x-gm-message-state:date:in-reply-to:message-id:mime-version
 :references:subject:from:to:cc;
 bh=msKDKUmtDWKJmYvE8CxOLhiyS9H8DVoQQreyfB+ekYk=;
 b=Z7VJmSpS3WXX8peVbArTQ1iHtxPPSsHz6U9JT4hL0NE0CfNf9fEECdo/f2SchF/cq6
 lfD1g7gvjKg677jlO+vIz0WrPFZW9hMlfnZOsoQO1JqWZIWFMF+4D6bqVhk1arfMcbaO
 ChElW3GCWtcW1RJv9pi2hgyridvsgEYpw0ReAHOvdvMxJTH/MJA2FUyrI8KN8tEQb0yB
 OTYMG8ja0fIS20mLZSoERSsnyXKktbAT4ag7JvH7cWp1UKlJXi9O66Uono4fXl4mKCbb
 areWFCdYfFVorEEK260K6r1jyDgxQp86qamsC7oCQWiN7Gq8DFqUbd0lw2opgUGVWWCT
 xQ3w==
X-Gm-Message-State: AOAM532gI379xnbgNVU0Bph8XxcQIW3ZbVAmSqyOS3pLZMp6EakdyK7a
 fZO4QYnusQ6aCzo+oJFGyk7QFcMs3PwkVetzA+HX0xrZOv8lb9CM5WrZJkhVDrZXIOG5BX8oGh1
 qNozFoTLEx27PBnkebMjdSD3abMRj/evazi7qWQSe1Xm/c0lW+v3J61nVHg1Hd/xV
X-Google-Smtp-Source: 
 ABdhPJyA7HVA5+gd4kXGFa/dC4es2lJxwe4eS4b6hyQt13L1hsNfLltVrQdfAamwYMF/aDXUDzVTcaq7RQ==
X-Received: from raxel-pw.c.googlers.com
 ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda])
 (user=raxel job=sendgmr) by 2002:ad4:45a8:: with SMTP id
 y8mr18312092qvu.49.1629435046104;
 Thu, 19 Aug 2021 21:50:46 -0700 (PDT)
Date: Fri, 20 Aug 2021 04:50:29 +0000
In-Reply-To: <20210820045030.3364156-1-raxel@google.com>
Message-Id: <20210820045030.3364156-9-raxel@google.com>
Mime-Version: 1.0
References: <20210820045030.3364156-1-raxel@google.com>
X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog
Subject: [PATCH v4 8/9] patch-detail: add label and button for comment
 addressed status
From: Raxel Gutierrez 
To: patchwork@lists.ozlabs.org
X-BeenThere: patchwork@lists.ozlabs.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Patchwork development 
List-Unsubscribe: ,
 
List-Archive: 
List-Post: 
List-Help: 
List-Subscribe: ,
 
Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org
Sender: "Patchwork"
 

Add new label to patch and cover comments to show the status of whether
they are addressed or not and add an adjacent button to allow users to
change the status of the comment. Only users that can edit the patch
(i.e. patch author, delegate, project maintainers) as well as comment
authors can change the status of a patch comment. For cover comments,
there are no delegates, so only maintainers and cover/cover comment
authors can edit the status of the cover comment. Before [1] and after
[2] images for reference.

Use new comment detail REST API endpoint to update the addressed field
value when "Addressed" or "Unaddressed" buttons are clicked. After a
successful request is made, the appearance of the comment status label
and buttons are toggled appropriately. For unsuccessful requests (e.g.
network errors prevents reaching the server), the error message is
populated to the page. A future improvement on this behavior is to add
a spinner to the button to provide a feedback that the request is in a
pending state until it's handled.

[1] https://imgur.com/3ZKzgjN
[2] https://imgur.com/hWZrrnM

Signed-off-by: Raxel Gutierrez 
---
 htdocs/css/style.css                          | 38 ++++++++++++
 htdocs/js/submission.js                       | 20 +++++++
 patchwork/templates/patchwork/submission.html | 60 +++++++++++++++----
 patchwork/views/patch.py                      |  4 +-
 4 files changed, 110 insertions(+), 12 deletions(-)

diff --git a/htdocs/css/style.css b/htdocs/css/style.css
index a2a2e3c3..9156aa6e 100644
--- a/htdocs/css/style.css
+++ b/htdocs/css/style.css
@@ -1,4 +1,5 @@
 :root {
+    --light-color:rgb(247, 247, 247);
     --success-color:rgb(92, 184, 92);
     --warning-color:rgb(240, 173, 78);
     --danger-color:rgb(217, 83, 79);
@@ -27,6 +28,10 @@ pre {
     top: 17em;
 }
 
+.hidden {
+    visibility: hidden;
+}
+
 /* Bootstrap overrides */
 
 .navbar-inverse .navbar-brand > a {
@@ -315,6 +320,39 @@ table.patch-meta tr th, table.patch-meta tr td {
     font-family: "DejaVu Sans Mono", fixed;
 }
 
+div[class^="comment-status-bar-"] {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+}
+
+.comment-status-label {
+    margin: 0px 8px;
+}
+
+button[class^=comment-action] {
+    background-color: var(--light-color);
+    border-radius: 4px;
+}
+
+.comment-action-addressed {
+    border-color: var(--success-color);
+}
+
+.comment-action-unaddressed {
+    border-color: var(--warning-color);
+}
+
+.comment-action-addressed:hover {
+    background-color: var(--success-color);
+    color: var(--light-color);
+}
+
+.comment-action-unaddressed:hover {
+    background-color: var(--warning-color);
+    color: var(--light-color);
+}
+
 .quote {
     color: #007f00;
 }
diff --git a/htdocs/js/submission.js b/htdocs/js/submission.js
index 9676f348..47cffc82 100644
--- a/htdocs/js/submission.js
+++ b/htdocs/js/submission.js
@@ -1,4 +1,7 @@
+import { updateProperty } from "./rest.js";
+
 $( document ).ready(function() {
+    const patchMeta = document.getElementById("patch-meta");
     function toggleDiv(link_id, headers_id, label_show, label_hide) {
         const link = document.getElementById(link_id)
         const headers = document.getElementById(headers_id)
@@ -14,6 +17,23 @@ $( document ).ready(function() {
         }
     }
 
+    $("button[class^='comment-action']").click((event) => {
+        const submissionType = patchMeta.dataset.submissionType;
+        const submissionId = patchMeta.dataset.submissionId;
+        const commentId = event.target.parentElement.dataset.commentId;
+        const url = `/api/${submissionType}/${submissionId}/comments/${commentId}/`;
+        const data = {'addressed': event.target.value} ;
+        const updateMessage = {
+            'error': "No comments updated",
+            'success': "1 comment(s) updated",
+        };
+        updateProperty(url, data, updateMessage).then(isSuccess => {
+            if (isSuccess) {
+                $("div[class^='comment-status-bar-'][data-comment-id='"+commentId+"']").toggleClass("hidden");
+            }
+        })
+    });
+
     // Click listener to show/hide headers
     document.getElementById("toggle-patch-headers").addEventListener("click", function() {
         toggleDiv("toggle-patch-headers", "patch-headers");
diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
index 36b15d0e..2238e82e 100644
--- a/patchwork/templates/patchwork/submission.html
+++ b/patchwork/templates/patchwork/submission.html
@@ -5,6 +5,7 @@
 {% load person %}
 {% load patch %}
 {% load static %}
+{% load utils %}
 
 {% block headers %}
   
@@ -19,7 +20,12 @@
   

{{ submission.name }}

- +
{% if submission.list_archive_url %} @@ -271,18 +277,50 @@ {% if forloop.first %}

Comments

{% endif %} - +{% is_editable item user as comment_is_editable %}
-
- {{ item.submitter|personify:project }} - {{ item.date }} UTC | - #{{ forloop.counter }} - -
-
-{{ item|commentsyntax }}
-
+
+ {{ item.submitter|personify:project }} + {{ item.date }} UTC | + #{{ forloop.counter }} + + {% if item.addressed %} +
+ {% else %} + + {% if item.addressed %} + +
+  {{ item|commentsyntax }}
+  
{% endfor %} diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py index 3e6874ae..00b0147f 100644 --- a/patchwork/views/patch.py +++ b/patchwork/views/patch.py @@ -109,7 +109,8 @@ def patch_detail(request, project_id, msgid): comments = patch.comments.all() comments = comments.select_related('submitter') - comments = comments.only('submitter', 'date', 'id', 'content', 'patch') + comments = comments.only('submitter', 'date', 'id', 'content', 'patch', + 'addressed') if patch.related: related_same_project = patch.related.patches.only( @@ -128,6 +129,7 @@ def patch_detail(request, project_id, msgid): patch.check_set.all().select_related('user'), ) context['submission'] = patch + context['editable'] = editable context['patchform'] = form context['createbundleform'] = createbundleform context['project'] = patch.project From patchwork Fri Aug 20 04:50:30 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1518919 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org; envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org; receiver=) Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20161025 header.b=eK36n4Ep; dkim-atps=neutral Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4GrTkP65Fwz9sWl for ; Fri, 20 Aug 2021 14:51:41 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4GrTkP4Z29z3cjX for ; Fri, 20 Aug 2021 14:51:41 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20161025 header.b=eK36n4Ep; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=flex--raxel.bounces.google.com (client-ip=2607:f8b0:4864:20::849; helo=mail-qt1-x849.google.com; envelope-from=3pzqfyqukczwn6tahckkcha.8kil6p8dskngheopo.kvh67o.knc@flex--raxel.bounces.google.com; receiver=) Authentication-Results: lists.ozlabs.org; dkim=pass (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20161025 header.b=eK36n4Ep; dkim-atps=neutral Received: from mail-qt1-x849.google.com (mail-qt1-x849.google.com [IPv6:2607:f8b0:4864:20::849]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 4GrTjP4kFsz3cHH for ; Fri, 20 Aug 2021 14:50:49 +1000 (AEST) Received: by mail-qt1-x849.google.com with SMTP id 7-20020ac856070000b0290292921115ecso4116574qtr.6 for ; Thu, 19 Aug 2021 21:50:49 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=vHv5SFkyYJGSiqIoJ88zOqvSgmzRsBX0kKJw9737d/I=; b=eK36n4EpizCfoH5AYSRJszzp2FHjw1GQdccj3eJu/cxCIHQCsJkjmP+6WBnswR+Hgo YRK0/y/gl0GXP2y6LyAVYuqQmLPbtzOq8fnQpZBCwYy70pV0w/ZSjZLZNaimNbE0bBfr dmSbYYtAf2Pu4rPOmLdSOjm+KQIiFdOPJA4AA/8ZoE4uaUKPwYIsM7KbjOMP9H6zsaLS 3cIh9r4XwxruoZMJjuCLfqWb+oXZV7iDx8IMjlq3w3Dh+h4k+i7nIn1bDThIXZzBYTeH gGjBnfqqa+9PCuBfHL/Hz6cJ0ZS4MeMaXgIsvPbym1tZWHju270jz2aqOcYx+FhxQplA cQPQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=vHv5SFkyYJGSiqIoJ88zOqvSgmzRsBX0kKJw9737d/I=; b=B4VGkRUNpkib+syMqjY2KAerkEUj23rWNv+UWptPTehwz4dxqnAQXmBeOyTo7eX2/S jI/jcg5x5f0eqqHrh3E7TRo5B1EucY6Z42uTDjActJZFN8gnJB0qMvJBqciodt1q2/cq lLTvlLaytAGCFKzOn6+mcqRXvYplDtsI1fM1nDIkyZMu1hI8+qlm60fQ+7tVi+jEi1VK mJTxmnst9hoOIBuVkX0LJLV1GhGKbFdvSM7YHU3uWl955MPrbTSQFYgATpfAKYe78r7z 46NaqpKpBDJxtLcbEip9v8XwqvN02KA0dzANb89FfJVWAu/nCGw7KHWD4Tv6ToZn33Us I3EA== X-Gm-Message-State: AOAM532wgoDa8xN5cUn07Y5WnjNrQQgYRx6NlGHXYpn6kzxD5G9ZX3v6 27iIFCwnjdXGjdZdAGDKJH/gqfm9dGD+O3tXY8ZhQiXQ81bsE79JlErgiO9/09o7oaYUbyiLKJX k9jkiKSm+MuQSSf+CZuUuVE2JXTrhzHdrRBjc+UnSKp2cnz9Kf8hM1ZJMCAk7N7wm X-Google-Smtp-Source: ABdhPJyeTl0ADdHlkVMhcP6ztK/pf1Eg15v9UQDHaw+9mLJpaE9gyJ2c/+dfbtivr6aMiRPaCcp4aZnACQ== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:10c4:: with SMTP id r4mr18360057qvs.58.1629435047554; Thu, 19 Aug 2021 21:50:47 -0700 (PDT) Date: Fri, 20 Aug 2021 04:50:30 +0000 In-Reply-To: <20210820045030.3364156-1-raxel@google.com> Message-Id: <20210820045030.3364156-10-raxel@google.com> Mime-Version: 1.0 References: <20210820045030.3364156-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [PATCH v4 9/9] docs: add release note for addressed/unaddressed comments From: Raxel Gutierrez To: patchwork@lists.ozlabs.org X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" Signed-off-by: Raxel Gutierrez --- ...ressed-patch-comments-bfe71689b6f35a22.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 releasenotes/notes/comment-detail-endpoint-for-addressed-unaddressed-patch-comments-bfe71689b6f35a22.yaml diff --git a/releasenotes/notes/comment-detail-endpoint-for-addressed-unaddressed-patch-comments-bfe71689b6f35a22.yaml b/releasenotes/notes/comment-detail-endpoint-for-addressed-unaddressed-patch-comments-bfe71689b6f35a22.yaml new file mode 100644 index 00000000..799fd879 --- /dev/null +++ b/releasenotes/notes/comment-detail-endpoint-for-addressed-unaddressed-patch-comments-bfe71689b6f35a22.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Patch comments and cover letter comments can be marked 'addressed' or 'unaddressed' to + reflect whether the comment has been addressed by the patch and cover letter submitter + or a reviewer. The current state of a comment is shown in the header when showing a + comment and users with edit permission can toggle the state using an adjacent button. +api: + - | + The API version has been updated to v1.3. + - | + A new REST API endpoint is available at ``/api/covers//comments//``. + This can be used to retrieve and update (e.g. ``addressed`` state) details about a specific + cover comment. + - | + A new REST API endpoint is available at ``/api/patches//comments//``. + This can be used to retrieve and update (e.g. ``addressed`` state) details about a specific + patch comment.
Message ID