From patchwork Mon Aug 23 14:58:44 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1519749 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=d2EaN3oJ; 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 4Gtb423fzCz9t0Y for ; Tue, 24 Aug 2021 00:59:14 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gtb422Y2sz2xsg for ; Tue, 24 Aug 2021 00:59:14 +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=d2EaN3oJ; 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::749; helo=mail-qk1-x749.google.com; envelope-from=3rlcjyqukcbkqzwdkfnnfkd.bnlozsbgvnqjkhrsr.nykzar.nqf@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=d2EaN3oJ; dkim-atps=neutral Received: from mail-qk1-x749.google.com (mail-qk1-x749.google.com [IPv6:2607:f8b0:4864:20::749]) (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 4Gtb3h5Yd5z2xff for ; Tue, 24 Aug 2021 00:58:55 +1000 (AEST) Received: by mail-qk1-x749.google.com with SMTP id d202-20020a3768d3000000b003d30722c98fso12008988qkc.10 for ; Mon, 23 Aug 2021 07:58:55 -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=1xB+G2uWdhUXdd0vLcSTGJmGH54UxB2dDB9IlancL9I=; b=d2EaN3oJzqqvgc4cVzxZHZGeJDUOyitgsN9yP7e6ExS+oRuyoACzD/Vuorh+T1A/o6 4NJcCv1hKvRcMDt/gD8zW/uldHug2xrJZlDSLw/quGU5EGvlLjduMSjhk2hhipYHs+tF kuyjOqjb2cW9iI8PdBFp7Dcyy2AzJGOtuNS7oOZ6uNulFfLo6TcBfggwomNOvPAzSCV7 MiemTFtZWNPFIFW/PWhxxaptVIjVMC5wRhNmLgYKxC2v3xwsY5tEha4+WMjtC2ugtKTM /yqAKSyYb0kfGZXug3W/XMsI+vDh+gXCFmCgRa6HkAl8LyrGDpkTdZT8XNQZ7eSgmYcv z8jg== 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=1xB+G2uWdhUXdd0vLcSTGJmGH54UxB2dDB9IlancL9I=; b=pcg6yysCv7iKiUsuty/MnTtOAtxMKuo7/5WRJOZo56fFN0jtNRGR50EjVP9dXlx17j 6tvN3KVNG4e3nPzcbrdTNQ8++lq45SBTaJZuc1FH19WpzrLY4iFeucw/VOF0A5NLUuy6 ljeyl9EjLNikNrN4mTwWQEEiSIzROKpa66Jevas5XTPaKr5C2WnIbTPKC9WqpM4Sb/I3 uU/wuYfOuxUqF13XU+y38lW2PT1C03+aF5Azj5oTzqTsd825qyfOohXkdWArGN+I4bDZ bLyFuZM7ohKqqVX8WESFwUGBeBLd3DI4dboAfGc0xskcbxlOKfwOBa4m3lFWIg0Mn+MP E41A== X-Gm-Message-State: AOAM533lOPumbPMOIzufdAhW0T005F9GYg6bhk9yG0ScmKCGzmzKUNN5 JzDuZHl+m5abInH4WbKBqc6Q2Hg3FcvPI7fOb/3VuF2/yq//RczKILlB9zi3NR+D1LJy2HJQDCC kT+0kUf24U1iRZgNleHgpFBX8KGeMYqA980ikNZmEbv9egPQvPqhPujmQQRElK6GN X-Google-Smtp-Source: ABdhPJzoIyTqU8gS/Re1sFmHrgWu2SIYNhX9c6kmu7KzXTzNrnznPQnC4QUAJrsl4eVcK2/U4furgf9MyA== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:1501:: with SMTP id e1mr5595730qvy.62.1629730732521; Mon, 23 Aug 2021 07:58:52 -0700 (PDT) Date: Mon, 23 Aug 2021 14:58:44 +0000 In-Reply-To: <20210823145847.3944896-1-raxel@google.com> Message-Id: <20210823145847.3944896-2-raxel@google.com> Mime-Version: 1.0 References: <20210823145847.3944896-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [RFC PATCH 1/4] patch-detail: add patch relation context 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" These changes are setup for the upcoming change that adds a patch relations table to the submission page. In particular: - Add unaddressed and addressed counts to patches and patch relations to be able to view the information in the patch relations table. - Change the current patch tags count filter to a function of the patch object so that it can be used by the patch relations tags count filter. - Add template filter that retrieves a summary of the existence of tags (e.g. A/R/T) for a given patch relation. - Add the project maintainers as context to be auto-generate them in an email in the case that patch relations need to be modified. Signed-off-by: Raxel Gutierrez --- patchwork/models.py | 36 +++++++++++++++++++ patchwork/templates/patchwork/submission.html | 4 +++ patchwork/templatetags/patch.py | 34 ++++++++++++------ patchwork/views/patch.py | 23 +++++++++--- 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/patchwork/models.py b/patchwork/models.py index 58e4c51e..b1d8c5bc 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -462,6 +462,14 @@ class Patch(SubmissionMixin): objects = PatchManager() + @property + def unaddressed_comments_count(self): + return self.comments.filter(addressed=False).count() + + @property + def addressed_comments_count(self): + return self.comments.filter(addressed=True).count() + @staticmethod def extract_tags(content, tags): counts = Counter() @@ -494,6 +502,18 @@ class Patch(SubmissionMixin): for tag in tags: self._set_tag(tag, counter[tag]) + def patch_tags_count(self): + counts = [] + titles = [] + for tag in [t for t in self.project.tags if t.show_column]: + count = getattr(self, tag.attr_name) + titles.append('%d %s' % (count, tag.name)) + if count == 0: + counts.append("-") + else: + counts.append(count) + return counts, titles + def save(self, *args, **kwargs): if not hasattr(self, 'state') or not self.state: self.state = get_default_initial_patch_state() @@ -950,6 +970,22 @@ class BundlePatch(models.Model): class PatchRelation(models.Model): + @property + def unaddressed_comments_total(self): + total = 0 + patches = self.patches.all() + for patch in patches: + total += patch.unaddressed_comments_count + return total + + @property + def addressed_comments_total(self): + total = 0 + patches = self.patches.all() + for patch in patches: + total += patch.addressed_comments_count + return total + def __str__(self): patches = self.patches.all() if not patches: diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html index 2238e82e..7dd6ae97 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -9,6 +9,10 @@ {% block headers %} + {% endblock %} {% block title %}{{submission.name}}{% endblock %} diff --git a/patchwork/templatetags/patch.py b/patchwork/templatetags/patch.py index 3837798d..c07c0bee 100644 --- a/patchwork/templatetags/patch.py +++ b/patchwork/templatetags/patch.py @@ -16,18 +16,25 @@ register = template.Library() @register.filter(name='patch_tags') def patch_tags(patch): - counts = [] - titles = [] - for tag in [t for t in patch.project.tags if t.show_column]: - count = getattr(patch, tag.attr_name) - titles.append('%d %s' % (count, tag.name)) - if count == 0: - counts.append("-") - else: - counts.append(str(count)) + counts, titles = patch.patch_tags_count() return mark_safe('%s' % ( ' / '.join(titles), - ' '.join(counts))) + ' '.join([str(x) for x in counts]))) + + +@register.filter(name='patch_relation_tags') +def patch_relation_tags(related_patches, project): + tags = [tag.abbrev for tag in project.tags] + tags_summary = ["-" for _ in range(len(tags))] + for patch in related_patches: + counts = patch.patch_tags_count()[0] + for i, count in enumerate(counts): + if count != '-': + # Replaces a non-zero tag count with tag abbreviation + # to indicate that existence of such tag in the set + # of related patches + tags_summary[i] = tags[i] + return mark_safe('%s' % (' '.join(tags_summary))) @register.filter(name='patch_checks') @@ -71,3 +78,10 @@ def patch_commit_display(patch): return mark_safe('%s' % (escape(fmt.format(commit)), escape(commit))) + + +# TODO: can be modularized into a utils.py templatetags file +# to get is_editable from any object +@register.filter(name='patch_is_editable') +def patch_is_editable(patch, user): + return patch.is_editable(user) diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py index 00b0147f..8e685add 100644 --- a/patchwork/views/patch.py +++ b/patchwork/views/patch.py @@ -40,7 +40,11 @@ def patch_detail(request, project_id, msgid): # redirect to cover letters where necessary try: - patch = Patch.objects.get(project_id=project.id, msgid=db_msgid) + # Current patch needs tag counts when no relation exists + patch_qs = Patch.objects.filter( + project_id=project.id, msgid=db_msgid + ).with_tag_counts(project) + patch = patch_qs.get() except Patch.DoesNotExist: covers = Cover.objects.filter( project_id=project.id, @@ -113,15 +117,19 @@ def patch_detail(request, project_id, msgid): 'addressed') if patch.related: - related_same_project = patch.related.patches.only( - 'name', 'msgid', 'project', 'related') + related_same_project = patch.related.patches.order_by('-id').only( + 'name', 'msgid', 'project', 'related').with_tag_counts(project) + related_ids = {'ids': [rp.id for rp in related_same_project]} # avoid a second trip out to the db for info we already have related_different_project = [ related_patch for related_patch in related_same_project if related_patch.project_id != patch.project_id ] else: - related_same_project = [] + # If no patch relation exists, then current patch is only related. + # Add tag counts to the patch to display in patch relation table. + related_same_project = [patch] + related_ids = {'ids': [patch.id]} related_different_project = [] context['comments'] = comments @@ -133,7 +141,14 @@ def patch_detail(request, project_id, msgid): context['patchform'] = form context['createbundleform'] = createbundleform context['project'] = patch.project + context['maintainers'] = { + 'maintainers': [ + m.user.email for m in patch.project.maintainer_project.all() + ] + } + context['patch_relation'] = patch.related context['related_same_project'] = related_same_project + context['related_ids'] = related_ids context['related_different_project'] = related_different_project return render(request, 'patchwork/submission.html', context) From patchwork Mon Aug 23 14:58:45 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1519750 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=uhu20546; 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 4Gtb491zVwz9sXN for ; Tue, 24 Aug 2021 00:59:21 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gtb484JxLz2y8P for ; Tue, 24 Aug 2021 00:59: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=uhu20546; 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::749; helo=mail-qk1-x749.google.com; envelope-from=3rbcjyqukcbowf2jqlttlqj.htrufyhm1twpqnxyx.t4qfgx.twl@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=uhu20546; dkim-atps=neutral Received: from mail-qk1-x749.google.com (mail-qk1-x749.google.com [IPv6:2607:f8b0:4864:20::749]) (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 4Gtb3h66hgz2xr7 for ; Tue, 24 Aug 2021 00:58:55 +1000 (AEST) Received: by mail-qk1-x749.google.com with SMTP id o4-20020ae9f504000000b003d39d97b227so12053209qkg.2 for ; Mon, 23 Aug 2021 07:58:55 -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=d4i48EsVUhaDMIsqwXtKWi9L9ULlaCYEtko5F5j+UlQ=; b=uhu20546Z0mSQ2kxAq08kC33bNagmass6WLy9D8DK0YSxKZdf3ptNtPh51J+4dLO+L 4iXv3srr/UK9EPw9doKQbBKYfh495jYvUKu8hrLMnDUUIpm5+GzXdwj2jHKjE1+WDG1l SoSdAFa8Bphi3Un9kWk/7QSscYOfKZhTEzucMBE240sXVOuvD68UOSBU5xh0oBuYdadG UgF1QQ0mHCvLsp1QaK8ucOFpmc11usVeikWm5cOdwf9JY2p0VZHiWqt+15/Jdz8iNSs2 fj+QA22DoTwg9vxubAI+rOMMeUdIlFwuRDNIu8ELpGsTHN+N0mLS/Wo7CDRLBXkL5VGF TsJQ== 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=d4i48EsVUhaDMIsqwXtKWi9L9ULlaCYEtko5F5j+UlQ=; b=mixPWk5kT7JWI6yEVxSnl+gsx3pIptn4uYOVNHYwvPMdJtALNa/RvRTXGHTU4yT/fj lQULA21jbDfYAxygPg1FRhw+g6IFS8LxEtrlsW3RoKE4EgxSM0qE31Cut/BNm8saPuxh JqxIkJcayIIRRtoV1sSkCwcMiujmOcWk/ZX+N1QL0mLBqy27HNf1xuhhc3fD0yxRDs0e mkTxmk8a9ZEfRG7Tf6p6FYl7PhSVKfkZaBQXC/tWs/ntwyKpoXfSpy3DUKUwNOO7vuf9 GaYyl5Z8ORbnZHUm2/moWNhkjuV8N6SezDsFgBZ0PFwt445KNwnCOjYvPlAkBANkvVLZ yIWg== X-Gm-Message-State: AOAM533DDBgJe6RTNYtAETWRR60/7JQ+CttlIfV2kBYu2giMKYrP3shx UJqvXryHaY9cXuce7sSo9gC7fwxcqtUfPdWrS7++V1gdnO9s3kcxUW33xh/SBUosOoLJ3trrkJV TSHnOjLHKk9VVKONTi8kVtynXgUJi5BexONnzF3zCcZNJRUrsRZ8FdTJSGjKGXuT5 X-Google-Smtp-Source: ABdhPJxiOJhzjQ8UrUc8UibE2mbcqJb+AbPpCiEBh7t06Uvrk/NlinqAZTms4C4LngeEzX0z4atofVuq7A== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:a06:: with SMTP id dw6mr9600204qvb.48.1629730733635; Mon, 23 Aug 2021 07:58:53 -0700 (PDT) Date: Mon, 23 Aug 2021 14:58:45 +0000 In-Reply-To: <20210823145847.3944896-1-raxel@google.com> Message-Id: <20210823145847.3944896-3-raxel@google.com> Mime-Version: 1.0 References: <20210823145847.3944896-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [RFC PATCH 2/4] patch-detail: add patch relations table 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" The motivation behind the patch relations table is to be able to provide information about a given version of a patch in relation to its other versions. The table indicates whether there exists unaddressed comments in other versions and can also show what's the most up-to-date version of a given patch. The information on what is unresolved throughout the related patches history helps reviewers and submitters keep track of and quickly locate what needs to be done for a patch. Image [1] for reference. This involves: - Add and style patch relations table with a summary row that shows the total unaddressed and addressed comments for the patch relation and the existence of various tags through their abbreviations (e.g. A/R/T). - Add collapse button that hides all patches except for the current patch (which is highlighted) and the patch relation summary row [2]. If the patch relation contains errors, users can use the "Wrong related patches?" link to address the issues. For users with patch edit permissions, the patch relation can be modified (i.e. add/remove patches) [3]. For users without permission, an email is prompted to email the maintainers of the project to ask them to fix the patch relation. However, the functionality to add/remove patches is introduced in an upcoming patch. Something that is left TODO is fix the email prompt to maintainers to include the mailing list for each specific project instead of the patchwork mailing list. Also, commas and whitespace characters from the patch subject are cut off in the subject line of the email which needs to be fixed. [1] https://i.imgur.com/cwYKiUx.png [2] https://i.imgur.com/4HGCTYp.png [3] https://i.imgur.com/Xyy18oL.png Signed-off-by: Raxel Gutierrez --- htdocs/css/style.css | 120 +++++++++++++++++- htdocs/js/submission.js | 41 ++++++ patchwork/templates/patchwork/submission.html | 92 ++++++++++++++ 3 files changed, 252 insertions(+), 1 deletion(-) diff --git a/htdocs/css/style.css b/htdocs/css/style.css index 9156aa6e..89c0f5c0 100644 --- a/htdocs/css/style.css +++ b/htdocs/css/style.css @@ -1,8 +1,14 @@ :root { --light-color:rgb(247, 247, 247); - --success-color:rgb(92, 184, 92); + --dark-color:rgb(41, 43, 44); + --dark-color-75:rgb(41, 43, 44, .75); + --dark-color-50:rgb(41, 43, 44, .5); + --dark-color-25:rgb(41, 43, 44, .25); --warning-color:rgb(240, 173, 78); + --success-color:rgb(92, 184, 92); + --success-color-75:rgb(92, 184, 92, .75); --danger-color:rgb(217, 83, 79); + --danger-color-75:rgb(217, 83, 79, .75); } h2 { @@ -301,6 +307,118 @@ table.patch-meta tr th, table.patch-meta tr td { color: #f7977a; } +.table-bl-border-radius { + border-bottom-left-radius: 4px +} + +.table-br-border-radius{ + border-bottom-right-radius: 4px +} + +.panel { + width: fit-content; + margin-top: 16px; +} + +.panel-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + font-weight: bold; + padding: 8px; +} + +#table-collapse-up-btn, #table-collapse-down-btn { + font-size: 16px; + color: var(--dark-color); + cursor: pointer; + margin-right: 4px; +} + +#table-collapse-up-btn:hover, #table-collapse-down-btn:hover { + color: var(--dark-color-75); +} + +.panel-actions-bar { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +#related-patch-actions-input { + font-weight: normal; + border: var(--dark-color-25) solid 1px; + border-radius: 4px; + background-color: white; + margin-left: 8px; + padding: 2px 8px; +} + +#related-patch-actions-input:focus { + outline: none; +} + +#add-patch-relation-btn { + color: var(--success-color); + font-size: 16px; + margin: 0px 8px; + cursor: pointer; +} + +#add-patch-relation-btn:hover { + color: var(--success-color-75); +} + +#remove-patch-relation-btn { + color: var(--danger-color); + font-size: 16px; + margin-right: 8px; + cursor: pointer; +} + +#remove-patch-relation-btn:hover { + color: var(--danger-color-75); +} + +#related-patches-table { + border: inherit; + border-collapse: separate; + border-radius: 0px 0px 4px 4px; +} + +.btn-copy { + background-color: var(--light-color); + border-radius: 4px; + transition: all 0.3s ease-in-out; +} + +.btn-copy:hover { + outline: none; + transform: translateY(-2px); + filter: opacity(75%) drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.4)); +} + +button[title="Copy to Clipboard"].btn-copy:active { + outline: none; + transform: translateY(-1px); + filter: opacity(100%); +} + +#current-related-patch { + font-weight: bold; +} + +.related-patches-header, .related-patches-footer { + border: transparent solid 1px; + border-radius: 4px 4px 0px 0px; + font-weight: 600; +} + +.table thead > .related-patches-header > th { + vertical-align: middle; + border-bottom: var(--dark-color-50) solid 1px; +} + .submission-message .meta { display: flex; align-items: center; diff --git a/htdocs/js/submission.js b/htdocs/js/submission.js index 47cffc82..23f73233 100644 --- a/htdocs/js/submission.js +++ b/htdocs/js/submission.js @@ -2,6 +2,15 @@ import { updateProperty } from "./rest.js"; $( document ).ready(function() { const patchMeta = document.getElementById("patch-meta"); + const actionsInput = document.getElementById("related-patch-actions-input"); + const collapseTableBtn = document.getElementById("table-collapse-up-btn"); + const expandTableBtn = document.getElementById("table-collapse-down-btn"); + const relatedActionsBar = document.getElementsByClassName("panel-actions-bar")[0]; + const maintainers = django_maintainers_data['maintainers']; + + // Resize related patches input to length of placeholder text (+1 accounts for last letter) + actionsInput.setAttribute('size', actionsInput.getAttribute('placeholder').length + 1); + function toggleDiv(link_id, headers_id, label_show, label_hide) { const link = document.getElementById(link_id) const headers = document.getElementById(headers_id) @@ -34,6 +43,38 @@ $( document ).ready(function() { }) }); + function toggleVisibility(elements) { + for (let elem of elements) { + elem.classList.toggle("hidden"); + } + } + + $("#patch-relation-issue").click((event) => { + if (relatedActionsBar.classList.contains("hidden") && is_editable) { + $(relatedActionsBar).toggleClass("hidden"); + $(event.target).toggleClass("hidden"); + event.preventDefault(); + } else if (!is_editable) { + // TODO: Fix commas (',') cutting off rest of patch subject + const patchSubject = $("#current-related-patch > td > a").text().trim(); + let maintainersList = ""; + for (let i = 0; i < maintainers.length; i++) { + if (i != maintainers.length-1) { + maintainersList += maintainers[i] + ";" + } else { + maintainersList += maintainers[i]; + } + } + event.target.href = `mailto:patchwork@lists.ozlabs.org?subject=[Patch Relations Fix]:%20${patchSubject}&cc=${maintainersList}` + } + }); + + // Click listener to collapse/expand related patches table + $(collapseTableBtn).add(expandTableBtn).click(function() { + const collapseRows = document.querySelectorAll("#related-patches-body > tr:not(.related-patches-footer, #current-related-patch)"); + toggleVisibility([...collapseRows, collapseTableBtn, expandTableBtn]); + }); + // 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 7dd6ae97..3c1d7bed 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -139,6 +139,98 @@ {% endif %} +
+
+
+ + + Patch Relations +
+
+ {% csrf_token %} + Wrong related patches? + +
+
+ + + + + + + + + + + {% for related_patch in related_same_project %} + {% if related_patch.name != submission.name %} + + {% else %} + + {% endif %} + {% if patch_relation %} + + + + {% if patch_relation %} + + {% else %} + + {% endif %} + + {% endfor %} + {% if patch_relation %} + + + + + + + {% endif %} + + +
+
{% if patchform %}
From patchwork Mon Aug 23 14:58:46 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1519751 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=vrPPQfLz; 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 4Gtb4H3SC9z9sRf for ; Tue, 24 Aug 2021 00:59:27 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gtb4H2RM0z2y6F for ; Tue, 24 Aug 2021 00:59: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=vrPPQfLz; 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=3rrcjyqukcbssbyfmhpphmf.dpnqbudixpslmjtut.p0mbct.psh@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=vrPPQfLz; 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 4Gtb3j6PsHz2xlF for ; Tue, 24 Aug 2021 00:58:57 +1000 (AEST) Received: by mail-qv1-xf4a.google.com with SMTP id u8-20020a0cec880000b029035825559ec4so12454809qvo.22 for ; Mon, 23 Aug 2021 07:58:57 -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=YX3B3pJBZ1if66Y8YtqsNze+oUD6adZatt9/s+wqXvw=; b=vrPPQfLz6J9eNC/Ta4b2Ss8cNHhHmveatEnahKNG/26qt1RQ7iD/J8gZSN1UYv+/Ej +dl9+mmgOpZJi95veKHFH+QJOdZ2jWs9s2b8l4Oc5kxZACee4B/8AFaSO5iJ0yJumfMl MEwyHxarg5iKOws5zk/F6Xq/ZFM32xhuTfsWrgTwrTFp+RxRbWuLUDKnyG8jqjWicDyL 1z9Xp/87ShTp0xMHBKirOWggM0R3A1WxcFZvmImogjXwWFz48TTJqp4yVuZuKySOTtN5 shNQnAODWZuQMW1olSsDEYl6FgmWfUVizL9EZI4L062fRoRw7gmkI5B+Dk9dGheORDrz CY6A== 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=YX3B3pJBZ1if66Y8YtqsNze+oUD6adZatt9/s+wqXvw=; b=Mf/FoHUYLl8pr7i0XKTcSTYHjBBgguYGrr8AFaAzH4ucnUxZ/vxJv3wm6yWiLIkP9f 5DExQ98MxS42bhbwgtGl3p+py6cz5uKAtEF0UPBegyJM8wlPplvpB51dsd3j6cCQqALh CHDlnzqEDPvrpVyv6nwRxf6BG202/4Pk0KSC+AjAH8gjFOtj/ZBcywm/llH1blg3oEHt JSBV6G2bWkqJ+8Skcy4hx2W99wfKNbnV9TCJmsdcqtkJRBlYli81SageWd7B6FK3Qi/M o8myeTCp1Qp/eQgsogzCUgZ6YYy+L2onbRKRsxxDVcJTSGIKQDNEaevCjwOiweMSeWOn jvmQ== X-Gm-Message-State: AOAM532WKJvLBX0UDTCtBBX48GOZSHOWX9VwSRzs+yhhXj8cdNXK1/P2 qEOFngZZnHC42ZAKJT0/2vp3hNpVUjzAJNGAePP+4cIjEyTHe3SQW/MgIbUonILtI2xMQEIuaK5 y4Fs26XksH5951yrLLsd0bChqI1SjtdSd/HWg7rsjyhkqoXBG311QkFCt6tfOruh4 X-Google-Smtp-Source: ABdhPJysbO+yrosy4gDMQCdZ9VpUm7rVABHhQtm20jaLPELwTF/0fha0Q5IyMB7OvHh+YdoIodGbwhEvDg== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:1382:: with SMTP id g2mr33202226qvz.14.1629730734555; Mon, 23 Aug 2021 07:58:54 -0700 (PDT) Date: Mon, 23 Aug 2021 14:58:46 +0000 In-Reply-To: <20210823145847.3944896-1-raxel@google.com> Message-Id: <20210823145847.3944896-4-raxel@google.com> Mime-Version: 1.0 References: <20210823145847.3944896-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [RFC PATCH 3/4] api: modularize patch relations handling 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" Move function that handles updates for patch relations for REST API calls outside of DRF serialized so that it can be reused by the patch relations table through form submission. The function is used in an upcoming patch that deals with the manual modification of the patch relations of a given patch. Signed-off-by: Raxel Gutierrez --- patchwork/api/patch.py | 161 +++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 77 deletions(-) diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py index a97a8820..ff7640b8 100644 --- a/patchwork/api/patch.py +++ b/patchwork/api/patch.py @@ -70,6 +70,87 @@ class PatchConflict(APIException): ) +def check_user_maintains_all(user, patches): + maintains = user.maintainer_projects.all() + if any(s.project not in maintains for s in patches): + detail = ( + 'At least one patch is part of a project you are not ' + 'maintaining.' + ) + raise PermissionDenied(detail=detail) + return True + + +def update_patch_relations(user, patch, data): + # Validation rules + # ---------------- + # + # Permissions: to change a relation: + # for all patches in the relation, current and proposed, + # the user must be maintainer of the patch's project + # Note that this has a ratchet effect: if you add a cross-project + # relation, only you or another maintainer across both projects can + # modify that relationship in _any way_. + # + # Break before Make: a patch must be explicitly removed from a + # relation before being added to another + # + # No Read-Modify-Write for deletion: + # to delete a patch from a relation, clear _its_ related patch, + # don't modify one of the patches that are to remain. + # + # (As a consequence of those two, operations are additive: + # if 1 is in a relation with [1,2,3], then + # patching 1 with related=[2,4] gives related=[1,2,3,4]) + + # Permissions: + # Must be maintainer of: + # - current patch + check_user_maintains_all(user, [patch]) + # - all patches currently in relation + # - all patches proposed to be in relation + errors = [] + related_patches = data.pop('related') + patches = set(related_patches) if related_patches else set() + if patch.related is not None: + patches = patches.union(patch.related.patches.all()) + check_user_maintains_all(user, patches) + + # handle deletion + if not related_patches: + # do not allow relations with a single patch + if patch.related and patch.related.patches.count() == 2: + patch.related.delete() + patch.related = None + patch.save() + return errors + + # break before make + relations = {patch.related for patch in patches if patch.related} + if len(relations) > 1: + return [PatchConflict()] + if relations: + relation = relations.pop() + else: + relation = None + if relation and patch.related is not None: + if patch.related != relation: + return [PatchConflict()] + + # apply + if relation is None: + relation = PatchRelation() + relation.save() + + for rp in patches: + rp.related = relation + rp.save() + patch.related = relation + patch.save() + + return errors + + class PatchListSerializer(BaseHyperlinkedModelSerializer): web_url = SerializerMethodField() @@ -184,87 +265,13 @@ class PatchDetailSerializer(PatchListSerializer): return super(PatchDetailSerializer, self).update( instance, validated_data) - related = validated_data.pop('related') - - # Validation rules - # ---------------- - # - # Permissions: to change a relation: - # for all patches in the relation, current and proposed, - # the user must be maintainer of the patch's project - # Note that this has a ratchet effect: if you add a cross-project - # relation, only you or another maintainer across both projects can - # modify that relationship in _any way_. - # - # Break before Make: a patch must be explicitly removed from a - # relation before being added to another - # - # No Read-Modify-Write for deletion: - # to delete a patch from a relation, clear _its_ related patch, - # don't modify one of the patches that are to remain. - # - # (As a consequence of those two, operations are additive: - # if 1 is in a relation with [1,2,3], then - # patching 1 with related=[2,4] gives related=[1,2,3,4]) - - # Permissions: - # Because we're in a serializer, not a view, this is a bit clunky user = self.context['request'].user.profile - # Must be maintainer of: - # - current patch - self.check_user_maintains_all(user, [instance]) - # - all patches currently in relation - # - all patches proposed to be in relation - patches = set(related['patches']) if related else {} - if instance.related is not None: - patches = patches.union(instance.related.patches.all()) - self.check_user_maintains_all(user, patches) - - # handle deletion - if not related['patches']: - # do not allow relations with a single patch - if instance.related and instance.related.patches.count() == 2: - instance.related.delete() - instance.related = None - return super(PatchDetailSerializer, self).update( - instance, validated_data) - - # break before make - relations = {patch.related for patch in patches if patch.related} - if len(relations) > 1: - raise PatchConflict() - if relations: - relation = relations.pop() - else: - relation = None - if relation and instance.related is not None: - if instance.related != relation: - raise PatchConflict() - - # apply - if relation is None: - relation = PatchRelation() - relation.save() - for patch in patches: - patch.related = relation - patch.save() - instance.related = relation - instance.save() - + errors = update_patch_relations(user, instance, validated_data) + if errors: + raise errors.pop() return super(PatchDetailSerializer, self).update( instance, validated_data) - @staticmethod - def check_user_maintains_all(user, patches): - maintains = user.maintainer_projects.all() - if any(s.project not in maintains for s in patches): - detail = ( - 'At least one patch is part of a project you are not ' - 'maintaining.' - ) - raise PermissionDenied(detail=detail) - return True - class Meta: model = Patch fields = PatchListSerializer.Meta.fields + ( From patchwork Mon Aug 23 14:58:47 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1519752 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=pU4o8R1R; 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 4Gtb4P5grwz9sXN for ; Tue, 24 Aug 2021 00:59:33 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gtb4P4FNDz2xt6 for ; Tue, 24 Aug 2021 00:59:33 +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=pU4o8R1R; 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=3r7cjyqukcbwtczgniqqing.eqorcvejyqtmnkuvu.q1ncdu.qti@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=pU4o8R1R; 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 4Gtb3k5Smsz2xlF for ; Tue, 24 Aug 2021 00:58:58 +1000 (AEST) Received: by mail-qv1-xf49.google.com with SMTP id i7-20020a056214030700b0036b565ee6c0so4672390qvu.3 for ; Mon, 23 Aug 2021 07:58:58 -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=XvA06BTSd552QZuqNENPW96zxVJFbmvDYSJybrl+Ric=; b=pU4o8R1RRTiKHXxVTTX81CzrY2h8VvWBx/DZtdWH2Yepzso3bNUVKc439O1ILzMHiA LgRJ38VHYPVua6WLZLO71QP/73VUmtGExijHMoB+V/90TxvRIT8e6tbVavg/oWc6J/tw PF0TymhBV+bhupr3cpWQ/I+rktEAN6n7qTKEKUzvf4tvaYOudVKLqHLRSoHSv08qWwvT lIrsNoXBJONd9rd375grUlGItmIZoaQxCSxNtmBDO+A1ot0BG+9eYcA8jR9OgW7jIxRN Fa+tvut3E27PCqoUbW9TkSgk1eOvvEBXthibhJ5N0Ua36t9m1xi4plOTlKIm4ib+7aUZ 2glg== 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=XvA06BTSd552QZuqNENPW96zxVJFbmvDYSJybrl+Ric=; b=e96+rQPMcZCPw/odvLYCVsNEELbdjyqrIVFDvUd+EILSBTk/AGlc+sxYMz89LpEQXj WSN0nAYeWh5Udd5jGeNlw8Ydy9o5yummkgDxdNj5nvQIuln8gFUd5VSNelhrg2C+cxwz Qy11WdHbPm+yYEsctErM4bYnz2mPAqGsNYnG9yLIAow1nQwfCCt4WlInqRtco6fUA1aB Z1YrV1mAn2J2zjJFpsvwhXat3NXLB1EH+s8WC6x+KQgfi3HHlIOPNNQOtB7hP0Wx98D0 5YX5COr5VoD7Fuj8Un0rDO3hR+P02i1UogzqaGi8Y5hQtSbbk4gO0yNZr80ogm+BtdXa sUDw== X-Gm-Message-State: AOAM5323ERuH8WxRvFlUlNpvusxga9Oifr2C7MGMDG1bCnpulNhu7DLy rGQ7WNEv+OGgO6wBGMzIIMF1T7G9YugGTU2uwTacO4hf0lsWGFIftLqsAuKJjkJugNXmPg/mxZP 5Y1SShOHXFIXexcRvB8nfDbxxaz58e5EGgm4Mh3Jm04WjVKoSmkx6a2/PQop9xIB+ X-Google-Smtp-Source: ABdhPJyvcQ8mxAx1M0pLvRGbB/DlH1Tp7InxJBv83iDKssb2L/BiQ5FKahibCo+Zl3skxaZMFKkMn/TDJg== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:11ad:: with SMTP id u13mr3196901qvv.8.1629730735648; Mon, 23 Aug 2021 07:58:55 -0700 (PDT) Date: Mon, 23 Aug 2021 14:58:47 +0000 In-Reply-To: <20210823145847.3944896-1-raxel@google.com> Message-Id: <20210823145847.3944896-5-raxel@google.com> Mime-Version: 1.0 References: <20210823145847.3944896-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [RFC PATCH 4/4] patch-detail: add functionality to add/remove related patches 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, patch relations can be modified using the REST API, but the Patchwork UI provides no tools that allow users to create and edit patch relations. This patch adds that as part of the patch relations table. The changes involve the following details: Add handling of user input to the add/remove related patches as such: - Parse user input of ids and msgids by commas and whitespace - Match parsed ids to patches in the db - Record invalid ids to report back to the user - Update patch relations based on selected option to add/remove - Add update/error messages to web page as feedback In the future, an automated system to relate patches together would be ideal, meaning that this functionality should be considered a fallback option when the automated system has faults. At the very least, this manual option serves as the basic function to create and modify patch relations. Signed-off-by: Raxel Gutierrez --- patchwork/views/__init__.py | 33 +++++++++++++++++++++++++++++++ patchwork/views/patch.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py index 3efe90cd..35c9c23c 100644 --- a/patchwork/views/__init__.py +++ b/patchwork/views/__init__.py @@ -6,6 +6,7 @@ from django.contrib import messages from django.shortcuts import get_object_or_404 from django.db.models import Prefetch +from django.core.exceptions import ObjectDoesNotExist from patchwork.filters import Filters from patchwork.forms import MultiplePatchForm @@ -323,3 +324,35 @@ def process_multiplepatch_form(request, form, action, patches, context): messages.warning(request, 'No patches updated') return errors + + +def get_patch_relations_data(patch, action, data): + related_input = data.get('related_input', '').strip() + related_ids = [id.strip('<> ') for id in related_input.split(",")] + # patches that match the parsed user input ids and ids that did not match + related_patches, invalid_ids = get_patches_id_msgid(related_ids) + + if action == 'remove-related': + related_patches = [ + p for p in patch.related.patches.all() + if p in related_patches + ] + return ({'related': related_patches}, invalid_ids) + + +def get_patches_id_msgid(ids): + patches = [] + invalid_ids = [] + for str_id in ids: + try: + id = int(str_id) + try: + patches.append(Patch.objects.get(id=id)) + except ObjectDoesNotExist: + invalid_ids.append(id) + except ValueError: + try: + patches.append(Patch.objects.get(msgid='<' + str_id + '>')) + except ObjectDoesNotExist: + invalid_ids.append(str_id) + return (patches, invalid_ids) diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py index 8e685add..92f9038a 100644 --- a/patchwork/views/patch.py +++ b/patchwork/views/patch.py @@ -19,8 +19,10 @@ from patchwork.models import Cover from patchwork.models import Patch from patchwork.models import Project from patchwork.views import generic_list +from patchwork.views import get_patch_relations_data from patchwork.views.utils import patch_to_mbox from patchwork.views.utils import series_patch_to_mbox +from patchwork.api.patch import update_patch_relations def patch_list(request, project_id): @@ -64,6 +66,7 @@ def patch_detail(request, project_id, msgid): form = None createbundleform = None + errors = [] if editable: form = PatchForm(instance=patch) @@ -97,6 +100,41 @@ def patch_detail(request, project_id, msgid): 'Failed to add patch "%s" to bundle "%s": ' 'patch is already in bundle' % ( patch.name, bundle.name)) + elif action in ('add-related', 'remove-related'): + changed_relations = 0 # used for update message count + data, invalid_ids = get_patch_relations_data( + patch, action, request.POST + ) + + if data['related']: # check if any ids matched + if action == 'add-related': + update_errors = update_patch_relations( + request.user.profile, patch, data + ) + errors.extend(update_errors) + if not update_errors: + changed_relations += 1 + elif action == 'remove-related': + for rp in data['related']: + # for removal, to-be removed patch(es)' + # relations are emptied + update_errors = update_patch_relations( + request.user.profile, rp, {'related': []} + ) + errors.extend(update_errors) + if not update_errors: + changed_relations += 1 + + errors.extend( + ['%s is not a valid patch/msg id' % pid for pid in invalid_ids] + ) + if changed_relations >= 1: + messages.success( + request, + '%d patch relation(s) updated' % changed_relations + ) + else: + messages.warning(request, 'No patch relations updated') # all other actions require edit privs elif not editable: @@ -150,6 +188,7 @@ def patch_detail(request, project_id, msgid): context['related_same_project'] = related_same_project context['related_ids'] = related_ids context['related_different_project'] = related_different_project + context['errors'] = errors return render(request, 'patchwork/submission.html', context)