From patchwork Mon Aug 23 18:28:31 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1519911 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=i17BHKGN; 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 4GtgkJ5RLRz9sWS for ; Tue, 24 Aug 2021 04:29:12 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4GtgkJ400cz2xtW for ; Tue, 24 Aug 2021 04:29:12 +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=i17BHKGN; 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=33egjyqukcu47qdu1w44w1u.s425q9sxc4701y898.4f1qr8.47w@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=i17BHKGN; 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 4Gtgjq5Yf6z2xfx for ; Tue, 24 Aug 2021 04:28:47 +1000 (AEST) Received: by mail-qv1-xf49.google.com with SMTP id q13-20020a0ce9cd000000b003608f06491fso13008793qvo.18 for ; Mon, 23 Aug 2021 11:28:47 -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=ZhsqsKzk0zVCJnE28+BrgIf/aLuN4mRPlo3JbqU1Sss=; b=i17BHKGNDrL4Jr37yCjynSODnyn6UIaKgIUZL9JT5YFpjhZdGso8UZqv/no/LkvPUK fWSBbZ5EoWE2CJVmnLxN6HREdCYtUe9/ZJbT/qhiLtJXMcDCDlIeCN27xQkRV4bUiJsn OJD75ANhhqgEsDBnEyY85VVuFbLBqoL9ibU2L47eI2AM2zuRXfKBN9JRo2I8pYWXuad9 0dXFwjHfXaKbJnEDU5HnGYbvZz1L8DtVHf95mWniuLtAVahE9r6x4/rFixhxE/MydoPu +oN5Pj65Z0DfLEpirVgagHWPPnTT+3QjcdmLKNdyDz11kBzz/QNrvlZ4uR0PyoI1mb4a rLgQ== 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=ZhsqsKzk0zVCJnE28+BrgIf/aLuN4mRPlo3JbqU1Sss=; b=hWpNcL1Keb0Oaa+6OmaRQL3T8JFb6i3JwdYPrlRxbboO1rvrGsE7D/zFXP6gW0LKhT zbGfqzqORIE9fPczXE+mUhMMwiv6YWIGyHOFu376GRtqFJdhmQZaiii34jyk8Kn5OkOO Db68FPzZPYuRJHIBjd+KJiG/XhH6BJU3f9cc8w3+yW9TTwd3/ZJwhQlyD1w/1a/UETHr qzaoWYI4Wn2bKko2lre67MrncHhZ8lTLyFIY5UjILi6IHd8VS63QvYaKQWh2Q/9q/2fQ bYCVF5zuOsKHu6bP3oPKZ0SLwkCMNJ9Zpk6PKs6J4XTyZDXGJCAIZ4S8tqWY0vy5GBq7 KgwQ== X-Gm-Message-State: AOAM532KKbd68IPZ/agyVqrVd4w2ssKTClvIk9l4NtTlPTslj5bE+PUn CpyNbLocveXPsvW8Ao0dUKLCTUVQ9hK6+niW0YkU65dBTHOH5IeUpaZbWa40mD3QH72dwiE1LGJ YkOnQeNIPJIc35PAKI0HcpLeHvcLYcXdCfG6qTJh31xplUkGIXGZSiCZDjiwSbJNp X-Google-Smtp-Source: ABdhPJzdEtdi9FVmpdhc7CA7giG2KfPqEp6ONlCfmJxBdi/cco/uw5cOdupQeSaxGBP6V0cPg2scTeepJg== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:ad4:51ca:: with SMTP id p10mr3857446qvq.27.1629743325299; Mon, 23 Aug 2021 11:28:45 -0700 (PDT) Date: Mon, 23 Aug 2021 18:28:31 +0000 In-Reply-To: <20210823182833.3976100-1-raxel@google.com> Message-Id: <20210823182833.3976100-4-raxel@google.com> Mime-Version: 1.0 References: <20210823182833.3976100-1-raxel@google.com> X-Mailer: git-send-email 2.33.0.rc2.250.ged5fa647cd-goog Subject: [PATCH v4 3/5] patch-list: move and refactor patch-forms 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 patch forms in patch-list and detail page to a new template file patch-forms.html and move them to the top of the patch-list page to improve their discoverability. Refactor forms.py, __init__.py, patch.py, and test_bundles.py files so that the shared bundle form in patch-forms.html works for both the patch-list and patch-detail pages. In particular, the changes normalize the behavior of the error and update messages of the patch forms and updates tests to reflect the changes. Overall, these changes make patch forms ready for change and more synchronized in their behavior. More specifically: - Previously patch forms changes were separated between the patch-detail and patch-list pages. Thus, desired changes to the patch forms required changes to patch-list.html, submission.html, and forms.py. So, the most important benefit to this change is that forms.py and patch-forms.html become the two places to adjust the forms to handle form validation and functionality as well as UI changes. - Previously the patch forms in patch-list.html handled error and update messages through views in patch.py, whereas the patch forms in submission.html handled the messages with forms.py. Now, with a single patch forms component in patch-forms.html, forms.py is set to handle the messages and handle form validation for both pages. Signed-off-by: Raxel Gutierrez Reviewed-by: Stephen Finucane --- patchwork/forms.py | 12 ++- .../patchwork/partials/patch-forms.html | 45 ++++++++ .../patchwork/partials/patch-list.html | 100 ++---------------- patchwork/templates/patchwork/submission.html | 89 +--------------- patchwork/tests/views/test_bundles.py | 26 ++--- patchwork/views/__init__.py | 71 +++++++------ patchwork/views/patch.py | 35 ++---- 7 files changed, 126 insertions(+), 252 deletions(-) create mode 100644 patchwork/templates/patchwork/partials/patch-forms.html diff --git a/patchwork/forms.py b/patchwork/forms.py index 24322c78..33103f4d 100644 --- a/patchwork/forms.py +++ b/patchwork/forms.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django import forms from django.db.models import Q +from django.core.exceptions import ValidationError from django.db.utils import ProgrammingError from patchwork.models import Bundle @@ -51,7 +52,7 @@ class EmailForm(forms.Form): class BundleForm(forms.ModelForm): name = forms.RegexField( - regex=r'^[^/]+$', min_length=1, max_length=50, label=u'Name', + regex=r'^[^/]+$', min_length=1, max_length=50, required=False, error_messages={'invalid': 'Bundle names can\'t contain slashes'}) class Meta: @@ -70,11 +71,16 @@ class CreateBundleForm(BundleForm): def clean_name(self): name = self.cleaned_data['name'] + if not name: + raise ValidationError('No bundle name was specified', + code="invalid") + count = Bundle.objects.filter(owner=self.instance.owner, name=name).count() if count > 0: - raise forms.ValidationError('A bundle called %s already exists' - % name) + raise ValidationError('A bundle called %(name)s already exists', + code="invalid", + params={'name': name}) return name diff --git a/patchwork/templates/patchwork/partials/patch-forms.html b/patchwork/templates/patchwork/partials/patch-forms.html new file mode 100644 index 00000000..5a824aaf --- /dev/null +++ b/patchwork/templates/patchwork/partials/patch-forms.html @@ -0,0 +1,45 @@ +
+{% if patchform %} +
+
+ {{ patchform.state.errors }} + {{ patchform.state }} +
+
+ {{ patchform.delegate.errors }} + {{ patchform.delegate }} +
+
+ {{ patchform.archived.errors }} + {{ patchform.archived.label_tag }} {{ patchform.archived }} +
+ +
+{% endif %} +{% if user.is_authenticated %} +
+
+ {{ createbundleform.name.errors }} + {{ createbundleform.name }} + +
+ {% if bundles %} +
+ + +
+ {% endif %} + {% if bundle %} +
+ + +
+ {% endif %} +
+{% endif %} +
diff --git a/patchwork/templates/patchwork/partials/patch-list.html b/patchwork/templates/patchwork/partials/patch-list.html index 19b019cd..aeb26aa8 100644 --- a/patchwork/templates/patchwork/partials/patch-list.html +++ b/patchwork/templates/patchwork/partials/patch-list.html @@ -10,8 +10,6 @@ {% include "patchwork/partials/filters.html" %} -{% include "patchwork/partials/pagination.html" %} - {% if order.editable %} @@ -31,19 +29,15 @@
{% endif %} -{% if page.paginator.long_page and user.is_authenticated %} - -{% endif %} - -
+ {% csrf_token %} - + {% include "patchwork/partials/patch-forms.html" %} + {% include "patchwork/partials/pagination.html" %} + +
@@ -163,7 +157,7 @@ {% for patch in page.object_list %} - + {% if user.is_authenticated %}
@@ -206,86 +200,6 @@ {% if page.paginator.count %} {% include "patchwork/partials/pagination.html" %} - -
- -{% if patchform %} -
-

Properties

- - - - - - - - - - - - - - - - - -
Change state: - {{ patchform.state }} - {{ patchform.state.errors }} -
Delegate to: - {{ patchform.delegate }} - {{ patchform.delegate.errors }} -
Archive: - {{ patchform.archived }} - {{ patchform.archived.errors }} -
- -
-
- -{% endif %} - -{% if user.is_authenticated %} -
-

Bundling

- - - - - - {% if bundles %} - - - - - {% endif %} - {% if bundle %} - - - - - {% endif %} -
Create bundle: - - -
Add to bundle: - - -
Remove from bundle: - - -
-
-{% endif %} - -
-
-
- {% endif %} diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html index b617d366..4b0e9601 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -135,91 +135,10 @@ {% endif %}
-
-{% if patchform %} -
-

Patch Properties

-
- {% csrf_token %} - - - - - - - - - - - - - - - - - -
Change state: - {{ patchform.state }} - {{ patchform.state.errors }} -
Delegate to: - {{ patchform.delegate }} - {{ patchform.delegate.errors }} -
Archived: - {{ patchform.archived }} - {{ patchform.archived.errors }} -
- -
-
-
-{% endif %} - -{% if createbundleform %} -
-

Bundling

- - - - - -{% if bundles %} - - - - -{% endif %} -
Create bundle: - {% if createbundleform.non_field_errors %} -
{{createbundleform.non_field_errors}}
- {% endif %} -
- {% csrf_token %} - - {% if createbundleform.name.errors %} -
{{createbundleform.name.errors}}
- {% endif %} - {{ createbundleform.name }} - -
-
Add to bundle: -
- {% csrf_token %} - - - -
-
- -
-{% endif %} - -
-
-
+
+ {% csrf_token %} + {% include "patchwork/partials/patch-forms.html" %} +
{% if submission.pull_url %}

Pull-request

diff --git a/patchwork/tests/views/test_bundles.py b/patchwork/tests/views/test_bundles.py index e265eb69..b9818f71 100644 --- a/patchwork/tests/views/test_bundles.py +++ b/patchwork/tests/views/test_bundles.py @@ -354,7 +354,7 @@ class BundleCreateFromListTest(BundleTestBase): def test_create_empty_bundle(self): newbundlename = 'testbundle-new' params = {'form': 'patch-list-form', - 'bundle_name': newbundlename, + 'name': newbundlename, 'action': 'Create', 'project': self.project.id} @@ -370,7 +370,7 @@ class BundleCreateFromListTest(BundleTestBase): patch = self.patches[0] params = {'form': 'patch-list-form', - 'bundle_name': newbundlename, + 'name': newbundlename, 'action': 'Create', 'project': self.project.id, 'patch_id:%d' % patch.id: 'checked'} @@ -394,7 +394,7 @@ class BundleCreateFromListTest(BundleTestBase): n_bundles = Bundle.objects.count() params = {'form': 'patch-list-form', - 'bundle_name': '', + 'name': '', 'action': 'Create', 'project': self.project.id, 'patch_id:%d' % patch.id: 'checked'} @@ -415,7 +415,7 @@ class BundleCreateFromListTest(BundleTestBase): patch = self.patches[0] params = {'form': 'patch-list-form', - 'bundle_name': newbundlename, + 'name': newbundlename, 'action': 'Create', 'project': self.project.id, 'patch_id:%d' % patch.id: 'checked'} @@ -440,7 +440,9 @@ class BundleCreateFromListTest(BundleTestBase): params) self.assertNotContains(response, 'Bundle %s created' % newbundlename) - self.assertContains(response, 'You already have a bundle called') + self.assertContains( + response, + 'A bundle called %s already exists' % newbundlename) self.assertEqual(Bundle.objects.count(), n_bundles) self.assertEqual(bundle.patches.count(), 1) @@ -452,7 +454,7 @@ class BundleCreateFromPatchTest(BundleTestBase): patch = self.patches[0] params = {'name': newbundlename, - 'action': 'createbundle'} + 'action': 'Create'} response = self.client.post( reverse('patch-detail', @@ -471,7 +473,7 @@ class BundleCreateFromPatchTest(BundleTestBase): patch = self.patches[0] params = {'name': newbundlename, - 'action': 'createbundle'} + 'action': 'Create'} response = self.client.post( reverse('patch-detail', @@ -584,7 +586,7 @@ class BundleAddFromPatchTest(BundleTestBase): def test_add_to_empty_bundle(self): patch = self.patches[0] - params = {'action': 'addtobundle', + params = {'action': 'Add', 'bundle_id': self.bundle.id} response = self.client.post( @@ -594,7 +596,7 @@ class BundleAddFromPatchTest(BundleTestBase): self.assertContains( response, - 'added to bundle "%s"' % self.bundle.name, + 'added to bundle %s' % self.bundle.name, count=1) self.assertEqual(self.bundle.patches.count(), 1) @@ -603,7 +605,7 @@ class BundleAddFromPatchTest(BundleTestBase): def test_add_to_non_empty_bundle(self): self.bundle.append_patch(self.patches[0]) patch = self.patches[1] - params = {'action': 'addtobundle', + params = {'action': 'Add', 'bundle_id': self.bundle.id} response = self.client.post( @@ -613,7 +615,7 @@ class BundleAddFromPatchTest(BundleTestBase): self.assertContains( response, - 'added to bundle "%s"' % self.bundle.name, + 'added to bundle %s' % self.bundle.name, count=1) self.assertEqual(self.bundle.patches.count(), 2) @@ -651,7 +653,7 @@ class BundleInitialOrderTest(BundleTestBase): # need to define our querystring explicity to enforce ordering params = {'form': 'patch-list-form', - 'bundle_name': newbundlename, + 'name': newbundlename, 'action': 'Create', 'project': self.project.id, } diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py index 7700b734..5da8046d 100644 --- a/patchwork/views/__init__.py +++ b/patchwork/views/__init__.py @@ -3,11 +3,14 @@ # # SPDX-License-Identifier: GPL-2.0-or-later +import json + from django.contrib import messages from django.shortcuts import get_object_or_404 from django.db.models import Prefetch from patchwork.filters import Filters +from patchwork.forms import CreateBundleForm from patchwork.forms import MultiplePatchForm from patchwork.models import Bundle from patchwork.models import BundlePatch @@ -108,46 +111,34 @@ class Order(object): # TODO(stephenfin): Refactor this to break it into multiple, testable functions -def set_bundle(request, project, action, data, patches, context): +def set_bundle(request, project, action, data, patches): # set up the bundle bundle = None user = request.user if action == 'create': - bundle_name = data['bundle_name'].strip() - if '/' in bundle_name: - return ['Bundle names can\'t contain slashes'] - - if not bundle_name: - return ['No bundle name was specified'] - - if Bundle.objects.filter(owner=user, name=bundle_name).count() > 0: - return ['You already have a bundle called "%s"' % bundle_name] - + bundle_name = data['name'].strip() bundle = Bundle(owner=user, project=project, name=bundle_name) - bundle.save() - messages.success(request, "Bundle %s created" % bundle.name) + create_bundle_form = CreateBundleForm(instance=bundle, + data=request.POST) + if create_bundle_form.is_valid(): + create_bundle_form.save() + add_bundle_patches(request, patches, bundle) + bundle.save() + messages.success(request, 'Bundle %s created' % bundle.name) + else: + formErrors = json.loads(create_bundle_form.errors.as_json()) + errors = [e['message'] for e in formErrors['name']] + return errors elif action == 'add': + if not data['bundle_id']: + return ['No bundle was selected'] bundle = get_object_or_404(Bundle, id=data['bundle_id']) + add_bundle_patches(request, patches, bundle) elif action == 'remove': bundle = get_object_or_404(Bundle, id=data['removed_bundle_id']) - - if not bundle: - return ['no such bundle'] - - for patch in patches: - if action in ['create', 'add']: - bundlepatch_count = BundlePatch.objects.filter(bundle=bundle, - patch=patch).count() - if bundlepatch_count == 0: - bundle.append_patch(patch) - messages.success(request, "Patch '%s' added to bundle %s" % - (patch.name, bundle.name)) - else: - messages.warning(request, "Patch '%s' already in bundle %s" % - (patch.name, bundle.name)) - elif action == 'remove': + for patch in patches: try: bp = BundlePatch.objects.get(bundle=bundle, patch=patch) bp.delete() @@ -158,10 +149,21 @@ def set_bundle(request, project, action, data, patches, context): request, "Patch '%s' removed from bundle %s\n" % (patch.name, bundle.name)) + return [] - bundle.save() - return [] +def add_bundle_patches(request, patches, bundle): + for patch in patches: + bundlepatch_count = BundlePatch.objects.filter(bundle=bundle, + patch=patch).count() + if bundlepatch_count == 0: + bundle.append_patch(patch) + bundle.save() + messages.success(request, "Patch '%s' added to bundle %s" % + (patch.name, bundle.name)) + else: + messages.warning(request, "Patch '%s' already in bundle %s" % + (patch.name, bundle.name)) def generic_list(request, project, view, view_args=None, filter_settings=None, @@ -216,6 +218,7 @@ def generic_list(request, project, view, view_args=None, filter_settings=None, data = None user = request.user properties_form = None + create_bundle_form = None if user.is_authenticated: # we only pass the post data to the MultiplePatchForm if that was @@ -225,19 +228,20 @@ def generic_list(request, project, view, view_args=None, filter_settings=None, data_tmp = data properties_form = MultiplePatchForm(project, data=data_tmp) + create_bundle_form = CreateBundleForm() if request.method == 'POST' and data.get('form') == 'patch-list-form': action = data.get('action', '').lower() # special case: the user may have hit enter in the 'create bundle' # text field, so if non-empty, assume the create action: - if data.get('bundle_name', False): + if data.get('name', False): action = 'create' ps = Patch.objects.filter(id__in=get_patch_ids(data)) if action in bundle_actions: - errors = set_bundle(request, project, action, data, ps, context) + errors = set_bundle(request, project, action, data, ps) elif properties_form and action == properties_form.action: errors = process_multiplepatch_form(request, properties_form, @@ -288,6 +292,7 @@ def generic_list(request, project, view, view_args=None, filter_settings=None, context.update({ 'page': paginator.current_page, 'patchform': properties_form, + 'createbundleform': create_bundle_form, 'project': project, 'order': order, }) diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py index 00b0147f..39ff9132 100644 --- a/patchwork/views/patch.py +++ b/patchwork/views/patch.py @@ -14,11 +14,11 @@ from django.urls import reverse from patchwork.forms import CreateBundleForm from patchwork.forms import PatchForm -from patchwork.models import Bundle 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 set_bundle from patchwork.views.utils import patch_to_mbox from patchwork.views.utils import series_patch_to_mbox @@ -60,6 +60,7 @@ def patch_detail(request, project_id, msgid): form = None createbundleform = None + errors = None if editable: form = PatchForm(instance=patch) @@ -71,34 +72,14 @@ def patch_detail(request, project_id, msgid): if action: action = action.lower() - if action == 'createbundle': - bundle = Bundle(owner=request.user, project=project) - createbundleform = CreateBundleForm(instance=bundle, - data=request.POST) - if createbundleform.is_valid(): - createbundleform.save() - bundle.append_patch(patch) - bundle.save() - createbundleform = CreateBundleForm() - messages.success(request, 'Bundle %s created' % bundle.name) - elif action == 'addtobundle': - bundle = get_object_or_404( - Bundle, id=request.POST.get('bundle_id')) - if bundle.append_patch(patch): - messages.success(request, - 'Patch "%s" added to bundle "%s"' % ( - patch.name, bundle.name)) - else: - messages.error(request, - 'Failed to add patch "%s" to bundle "%s": ' - 'patch is already in bundle' % ( - patch.name, bundle.name)) - - # all other actions require edit privs + if action in ['create', 'add']: + errors = set_bundle(request, project, action, + request.POST, [patch]) + elif not editable: return HttpResponseForbidden() - elif action is None: + elif action == 'update': form = PatchForm(data=request.POST, instance=patch) if form.is_valid(): form.save() @@ -135,6 +116,8 @@ def patch_detail(request, project_id, msgid): context['project'] = patch.project context['related_same_project'] = related_same_project context['related_different_project'] = related_different_project + if errors: + context['errors'] = errors return render(request, 'patchwork/submission.html', context)