From patchwork Thu Jul 22 17:12: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: 1508809 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=Xn7sltO5; 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 4GVzYg0cXqz9sRR for ; Fri, 23 Jul 2021 03:13:27 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4GVzYf6f16z304H for ; Fri, 23 Jul 2021 03:13:26 +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=Xn7sltO5; 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=3lqf5yaukcxegpmtavddvat.rdbepirwldgzaxhih.doapqh.dgv@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=Xn7sltO5; 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 4GVzYY19J8z30F4 for ; Fri, 23 Jul 2021 03:13:21 +1000 (AEST) Received: by mail-qv1-xf4a.google.com with SMTP id hf7-20020a0562140e87b02902dc988b8675so87100qvb.3 for ; Thu, 22 Jul 2021 10:13:20 -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:content-transfer-encoding; bh=v/bQVs9dn7CFxyTaW7falWL2qvj4pbd6gD8Ubz1+BiA=; b=Xn7sltO5ZA8fIBCPnqPHF+xupzqrbXPMWTKojcbtx+LrT7ZHmb+gO2c/z2qR8lzt/f NYGGHiWenfEkLPRSQcrFrcYIzD+tj87Ks342rYRnfwVrMObWlro9jKMKYSuZFIa+6gjj 2Onp18OjOfM7+2aVEgOqF6KoxRIRkM4USs6dAnk20qF+X2kMvBv+gb7wBQTUqGcC2AMG dT8csNqITkPc0/2H4hMfXegoSY+YeNFjZi+f/k+coJwijvM8A6RxlBYe7fj+xWaW9oyx 0B8iNvjPhsv84nCosKQc+sxdPMjX8NIta/E6a20tKmYzGjF8yhiEIEbVaUM7XLz6tjvm 34VQ== 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:content-transfer-encoding; bh=v/bQVs9dn7CFxyTaW7falWL2qvj4pbd6gD8Ubz1+BiA=; b=As/AZnHhPgptw++F35jwHaVCHFD6OlRVw6inQPKUvkXPT53U7/G2q9LaiHQUMIrSZV 88jxL+XcADSDYxL/dppkOFYuHdbriS2zQNHMx5SA4IJ7CPzU9LA++x0G4vRLNiGhGDT9 It57iw3kgnAxuAfHOykHIiT9wKNI8y1DrcpGtU8vTJG5YIQf+rSjQ5WS4gLhFZaTRw0C LBhapJCCUL+CROImiwi2mcsrqp3S6/niuPPli9K95QApPUui1Fr/VEZaV+/kmQxfo99y rgAr54RjQyR0sAW/cy/Vgj98rqiWgPlQBjK3O3SZxqKbJ3hpHGtCq687d7hvbluU6uOU 0jIw== X-Gm-Message-State: AOAM532dF470G0H+n4lNRjg2cHPbUJo/fyF2o5Nu+Jo2SPXGy3MnCdjN eIdO7dCoYYGqx7PXd0T8Cnda6EKevHXfG5EffzVEKmQrivXWOwFU83cjFxyaX+0SFMoM7mOxtlr tVhXbcQuI2CQ9cZCmqtvjGX6OTGf2fC6jq5U+OHvOZeemsYut5Hi7+vWRDemCXV1N X-Google-Smtp-Source: ABdhPJwbUcBzTC8BUa1xwHkDSB+2rfP8aPlAhcAECq/ZIHWziudSjjmWYDJTe+gnSKtGWROkSQ0onMeuxw== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:d08:: with SMTP id 8mr820505qvh.32.1626973998023; Thu, 22 Jul 2021 10:13:18 -0700 (PDT) Date: Thu, 22 Jul 2021 17:12:47 +0000 In-Reply-To: <20210722171251.2554142-1-raxel@google.com> Message-Id: <20210722171251.2554142-2-raxel@google.com> Mime-Version: 1.0 References: <20210722171251.2554142-1-raxel@google.com> X-Mailer: git-send-email 2.32.0.432.gabb21c7263-goog Subject: [PATCH v2 1/5] static: add JS Cookie Library to get csrftoken for fetch requests 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, requests are made only through form submission and the csrftoken is added to templates using {% csrf_token %}. Following Django docs [1], the library is useful to add csrftoken when making requests in JavaScript. [1] https://docs.djangoproject.com/en/3.2/ref/csrf/#ajax Signed-off-by: Raxel Gutierrez Reviewed-by: Jonathan Nieder --- htdocs/README.rst | 9 +++++++++ htdocs/js/js.cookie.min.js | 3 +++ templates/base.html | 1 + 3 files changed, 13 insertions(+) create mode 100644 htdocs/js/js.cookie.min.js diff --git a/htdocs/README.rst b/htdocs/README.rst index 62f15c2..fa1616c 100644 --- a/htdocs/README.rst +++ b/htdocs/README.rst @@ -122,6 +122,15 @@ js :GitHub: jQuery plug-in to drag and drop rows in HTML tables :Version: ??? +``js.cookie.min.js`` + + Library used to handle cookies. + + This is used to get the ``csrftoken`` cookie for AJAX requests in JavaScript. + + :GitHub: https://github.com/js-cookie/js-cookie/ + :Version: 2.2.1 + ``selectize.min.js`` Selectize is the hybrid of a ``textbox`` and `` + +
+ {% include "patchwork/partials/patch-forms.html" %} + {% include "patchwork/partials/pagination.html" %} +
@@ -174,9 +165,9 @@ $(document).ready(function() { {% for patch in page.object_list %} - + {% if user.is_authenticated %} - {% endif %} @@ -188,24 +179,24 @@ $(document).ready(function() { {% endif %} - - - - - - - - + + + + + + {% empty %} @@ -217,86 +208,6 @@ $(document).ready(function() { {% if page.paginator.count %} {% include "patchwork/partials/pagination.html" %} - -
- -{% if patchform %} -
-

Properties

-
+ + {{ patch.name|default:"[no subject]"|truncatechars:100 }} + {% if patch.series %} {{ patch.series|truncatechars:100 }} {% endif %} {{ patch|patch_tags }}{{ patch|patch_checks }}{{ patch.date|date:"Y-m-d" }}{{ patch.submitter|personify:project }}{{ patch.delegate.username }}{{ patch.state }}{{ patch|patch_tags }}{{ patch|patch_checks }}{{ patch.date|date:"Y-m-d" }}{{ patch.submitter|personify:project }}{{ patch.delegate.username }}{{ patch.state }}
- - - - - - - - - - - - - - - - -
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 978559b..17255ee 100644 --- a/patchwork/templates/patchwork/submission.html +++ b/patchwork/templates/patchwork/submission.html @@ -27,6 +27,8 @@ function toggle_div(link_id, headers_id, label_show, label_hide) } +{% include "patchwork/partials/errors.html" %} +
{% include "patchwork/partials/download-buttons.html" %}

{{ submission.name }}

@@ -149,91 +151,10 @@ function toggle_div(link_id, headers_id, label_show, label_hide) {% 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 6a74409..2233c21 100644 --- a/patchwork/tests/views/test_bundles.py +++ b/patchwork/tests/views/test_bundles.py @@ -146,7 +146,7 @@ class BundleUpdateTest(BundleTestBase): data = { 'form': 'bundle', 'action': 'update', - 'name': newname, + 'bundle_name': newname, 'public': '', } response = self.client.post(bundle_url(self.bundle), data) @@ -159,7 +159,7 @@ class BundleUpdateTest(BundleTestBase): data = { 'form': 'bundle', 'action': 'update', - 'name': self.bundle.name, + 'bundle_name': self.bundle.name, 'public': 'on', } response = self.client.post(bundle_url(self.bundle), data) @@ -243,7 +243,7 @@ class BundlePublicModifyTest(BundleTestBase): data = { 'form': 'bundle', 'action': 'update', - 'name': newname, + 'bundle_name': newname, } self.bundle.name = oldname self.bundle.save() @@ -353,7 +353,7 @@ class BundleCreateFromListTest(BundleTestBase): def test_create_empty_bundle(self): newbundlename = 'testbundle-new' - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'bundle_name': newbundlename, 'action': 'Create', 'project': self.project.id} @@ -369,7 +369,7 @@ class BundleCreateFromListTest(BundleTestBase): newbundlename = 'testbundle-new' patch = self.patches[0] - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'bundle_name': newbundlename, 'action': 'Create', 'project': self.project.id, @@ -393,7 +393,7 @@ class BundleCreateFromListTest(BundleTestBase): n_bundles = Bundle.objects.count() - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'bundle_name': '', 'action': 'Create', 'project': self.project.id, @@ -414,7 +414,7 @@ class BundleCreateFromListTest(BundleTestBase): newbundlename = 'testbundle-dup' patch = self.patches[0] - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'bundle_name': newbundlename, 'action': 'Create', 'project': self.project.id, @@ -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) @@ -451,8 +453,8 @@ class BundleCreateFromPatchTest(BundleTestBase): newbundlename = 'testbundle-new' patch = self.patches[0] - params = {'name': newbundlename, - 'action': 'createbundle'} + params = {'bundle_name': newbundlename, + 'action': 'Create'} response = self.client.post( reverse('patch-detail', @@ -470,8 +472,8 @@ class BundleCreateFromPatchTest(BundleTestBase): newbundlename = self.bundle.name patch = self.patches[0] - params = {'name': newbundlename, - 'action': 'createbundle'} + params = {'bundle_name': newbundlename, + 'action': 'Create'} response = self.client.post( reverse('patch-detail', @@ -489,7 +491,7 @@ class BundleAddFromListTest(BundleTestBase): def test_add_to_empty_bundle(self): patch = self.patches[0] - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'action': 'Add', 'project': self.project.id, 'bundle_id': self.bundle.id, @@ -509,7 +511,7 @@ class BundleAddFromListTest(BundleTestBase): def test_add_to_non_empty_bundle(self): self.bundle.append_patch(self.patches[0]) patch = self.patches[1] - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'action': 'Add', 'project': self.project.id, 'bundle_id': self.bundle.id, @@ -538,7 +540,7 @@ class BundleAddFromListTest(BundleTestBase): count = self.bundle.patches.count() patch = self.patches[0] - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'action': 'Add', 'project': self.project.id, 'bundle_id': self.bundle.id, @@ -559,7 +561,7 @@ class BundleAddFromListTest(BundleTestBase): count = self.bundle.patches.count() patch = self.patches[0] - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'action': 'Add', 'project': self.project.id, 'bundle_id': self.bundle.id, @@ -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) @@ -650,7 +652,7 @@ class BundleInitialOrderTest(BundleTestBase): newbundlename = 'testbundle-new' # need to define our querystring explicity to enforce ordering - params = {'form': 'patchlistform', + params = {'form': 'patch-list-form', 'bundle_name': newbundlename, 'action': 'Create', 'project': self.project.id, diff --git a/patchwork/tests/views/test_patch.py b/patchwork/tests/views/test_patch.py index 1a1243c..483ab99 100644 --- a/patchwork/tests/views/test_patch.py +++ b/patchwork/tests/views/test_patch.py @@ -304,7 +304,7 @@ class PatchViewTest(TestCase): class PatchUpdateTest(TestCase): - properties_form_id = 'patchform-properties' + properties_form_id = 'patch-form-properties' def setUp(self): self.project = create_project() @@ -318,7 +318,7 @@ class PatchUpdateTest(TestCase): self.base_data = { 'action': 'Update', 'project': str(self.project.id), - 'form': 'patchlistform', + 'form': 'patch-list-form', 'archived': '*', 'delegate': '*', 'state': '*' diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py index 3efe90c..178d185 100644 --- a/patchwork/views/__init__.py +++ b/patchwork/views/__init__.py @@ -2,6 +2,7 @@ # Copyright (C) 2008 Jeremy Kerr # # SPDX-License-Identifier: GPL-2.0-or-later +import json from django.contrib import messages from django.shortcuts import get_object_or_404 @@ -9,6 +10,7 @@ from django.db.models import Prefetch from patchwork.filters import Filters from patchwork.forms import MultiplePatchForm +from patchwork.forms import CreateBundleForm from patchwork.models import Bundle from patchwork.models import BundlePatch from patchwork.models import Patch @@ -16,7 +18,6 @@ from patchwork.models import Project from patchwork.models import Check from patchwork.paginator import Paginator - bundle_actions = ['create', 'add', 'remove'] @@ -108,46 +109,35 @@ 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 = 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() + addBundlePatches(request, patches, bundle) + bundle.save() + create_bundle_form = CreateBundleForm() + messages.success(request, 'Bundle %s created' % bundle.name) + else: + formErrors = json.loads(create_bundle_form.errors.as_json()) + errors = [formErrors['name'][0]['message']] + 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']) + addBundlePatches(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 +148,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 addBundlePatches(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,17 +217,20 @@ 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 # the actual form submitted data_tmp = None - if data and data.get('form', '') == 'patchlistform': + if data and data.get('form', '') == 'patch-list-form': data_tmp = data properties_form = MultiplePatchForm(project, data=data_tmp) + if request.user.is_authenticated: + create_bundle_form = CreateBundleForm() - if request.method == 'POST' and data.get('form') == 'patchlistform': + 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' @@ -237,7 +241,7 @@ def generic_list(request, project, view, view_args=None, filter_settings=None, 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 3e6874a..5ef6916 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,30 +72,10 @@ 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() @@ -133,6 +114,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) From patchwork Thu Jul 22 17:12:49 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1508811 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=G9cQAfDi; 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 4GVzYr1wxbz9sRR for ; Fri, 23 Jul 2021 03:13:36 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4GVzYr10Vwz30Jf for ; Fri, 23 Jul 2021 03:13:36 +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=G9cQAfDi; 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=3mkf5yaukcxmirovcxffxcv.tfdgrktynfibczjkj.fqcrsj.fix@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=G9cQAfDi; 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 4GVzYb3f8qz30Cd for ; Fri, 23 Jul 2021 03:13:23 +1000 (AEST) Received: by mail-qk1-x74a.google.com with SMTP id h22-20020a05620a13f6b02903b94aaa0909so4581881qkl.15 for ; Thu, 22 Jul 2021 10:13:23 -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=+GgBjVmdgDR9Oe4Ovm4XJT9/d0WTTG9IOLj9PqK2H7I=; b=G9cQAfDi924tokWW96ZcK/SxS5VoiEGrAsGcT8I3EeVSsgNyOAp/HldXhHrd/teRQP FFhA0tP8z6ys/DjND9bOQGM3LGts6gNbVb3wDOlm1pMBT2DwDozOoZJZJt0FQyzPmtTs mUstGDrY+15xheJSyvuGa4o5GybMSSIB7SMrLg9EcStPeK5gHKvCLRJAxpSZ10YVo7Hp 6WwTkyVFYfwj+y9CWH2Tnx1UIaoJgiphmD2NwfKh6fQszjOXuimxRSjaAjWzTkdG/ASj ENX4Rpfe4XYHA1PHHpZX/pfrXba7t7curW0tN5suB0HOc+t8o9TNsa3PbSLJA3RL5lCd IMqA== 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=+GgBjVmdgDR9Oe4Ovm4XJT9/d0WTTG9IOLj9PqK2H7I=; b=XrcN86WeBbtCzULD3ugA5kDiiUbXMd1n3mhH0T4sfIvwBnV/dE3WOZzrOXOrnoMEdP o7COTJPCd5Uq7FJ3bCqP5qRqj1hG8tspe2V5EWtehjt2V1RFtQCdmuD+lnNLSalMb2s1 t6HG/k27gg0Gx68cHR6V7+LhTaelW5wl4W41ASHzbhTYmGpbSxLXj2iEwOqrQSP6rowS tvpdbCmTaRlNKeJo1GYu/2wxyP+cFMJqFTTcTnj9qNy+cnft4gJaCbCeQm4vFsamv9Vk QNQtrV85ZtFB6GNiihmyJ4hWSPcayXQQZhSIB+6J7JGZVPVZFwQO4lu04sh8nmy5AqpU NluA== X-Gm-Message-State: AOAM533XV2wHW5PDbS72fL4C93YPaKFCXxoPYD0FnzILJ/9byn5Kcmgy pgr6acOJpM7XZxVn8Z55K3nPKagihb3QFRxc1da2vo6chBYseM25RHcoWgkfr1U1yGerH/swL+G 2T3vNfQARHzABj2jwhhwf/IWWSvjPKQ3KQq9LW6zUys2Vqt6eIbVWm7CEb0TU7BB+ X-Google-Smtp-Source: ABdhPJxJbcv6pMOSVoGSUqrY5PjBqz5SpYF7NTzKaMpcg/DdFhqFcMLp3KarNBRE6mLIXIuMrN/zZduAXA== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:3004:: with SMTP id ke4mr976381qvb.52.1626974000646; Thu, 22 Jul 2021 10:13:20 -0700 (PDT) Date: Thu, 22 Jul 2021 17:12:49 +0000 In-Reply-To: <20210722171251.2554142-1-raxel@google.com> Message-Id: <20210722171251.2554142-4-raxel@google.com> Mime-Version: 1.0 References: <20210722171251.2554142-1-raxel@google.com> X-Mailer: git-send-email 2.32.0.432.gabb21c7263-goog Subject: [PATCH v2 3/5] patch-list: style modification forms as an action bar 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" Added styling to the new patch list html code to make the change property and bundle action forms more usable. Before [1] and after [2] images for reference. [1] https://imgur.com/Pzelipp [2] https://imgur.com/UtNJXuf Signed-off-by: Raxel Gutierrez Reviewed-by: Jonathan Nieder --- htdocs/css/style.css | 77 ++++++++++++++++++++++++++++++++++++-------- patchwork/forms.py | 44 +++++++++++++++++++------ 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/htdocs/css/style.css b/htdocs/css/style.css index 1bcc93e..9982f92 100644 --- a/htdocs/css/style.css +++ b/htdocs/css/style.css @@ -1,3 +1,7 @@ +:root { + --light-color: #F7F7F7; +} + h2 { font-size: 25px; margin: 18px 0 18px 0; @@ -122,10 +126,6 @@ a.colinactive:hover { div.filters { } -div.patch-forms { - margin-top: 1em; -} - /* list order manipulation */ table.patchlist tr.draghover { @@ -149,7 +149,7 @@ input#reorder-change { .paginator { text-align: right; clear: both; - margin: 8px 0 15px; + margin: 8px 0 15px; } .paginator .prev-na, @@ -346,13 +346,62 @@ table.bundlelist td padding-right: 2em; } +.patch-list-actions { + width: 100%; + display: inline-flex; + flex-wrap: wrap; + justify-content: space-between; +} + /* forms that appear for a patch */ +.patch-forms { + display: inline-flex; + flex-wrap: wrap; + margin: 16px 0px; +} + div.patch-form { - border: thin solid #080808; - padding-left: 0.6em; - padding-right: 0.6em; - float: left; - margin: 0.5em 5em 0.5em 10px; + display: flex; + flex-wrap: wrap; + align-items: center; +} + +select[class^=change-property-], .archive-patch-select, .add-bundle { + padding: 4px; + margin-right: 8px; + box-sizing: border-box; + border-radius: 4px; + background-color: var(--light-color); +} + +#patch-form-archive { + display: flex; + align-items: center; + margin-right: 4px; +} + +#patch-form-archive > label { + margin: 0px; +} + +#patch-form-archive > select, #patch-form-archive > input { + margin: 0px 4px 0px 4px; +} + +.patch-form-submit { + font-weight: bold; + padding: 4px; +} + +#patch-form-bundle, #add-to-bundle, #remove-bundle { + margin-left: 16px; +} + +.create-bundle { + padding: 4px; + margin-right: 8px; + box-sizing: border-box; + border-radius: 4px; } div.patch-form h3 { @@ -371,15 +420,17 @@ div.patch-form ul { margin-top: 0em; } -/* forms */ -table.form { +.create-bundle { + padding: 4px; + margin-right: 8px; + box-sizing: border-box; + border-radius: 4px; } span.help_text { font-size: 80%; } - table.form td { padding: 0.6em; vertical-align: top; diff --git a/patchwork/forms.py b/patchwork/forms.py index 7f1fd31..9727932 100644 --- a/patchwork/forms.py +++ b/patchwork/forms.py @@ -54,7 +54,10 @@ class BundleForm(forms.ModelForm): field_mapping = {'name': 'bundle_name'} name = forms.RegexField( regex=r'^[^/]+$', min_length=1, max_length=50, required=False, - error_messages={'invalid': 'Bundle names can\'t contain slashes'}) + error_messages={'invalid': 'Bundle names can\'t contain slashes'}, + widget=forms.TextInput( + attrs={'class': 'create-bundle', + 'placeholder': 'Bundle name'})) # Maps form fields 'name' attr rendered in HTML element def add_prefix(self, field_name): @@ -126,18 +129,28 @@ class PatchForm(forms.ModelForm): def __init__(self, instance=None, project=None, *args, **kwargs): super(PatchForm, self).__init__(instance=instance, *args, **kwargs) self.fields['delegate'] = forms.ModelChoiceField( - queryset=_get_delegate_qs(project, instance), required=False) + queryset=_get_delegate_qs(project, instance), + widget=forms.Select(attrs={'class': 'change-property-delegate'}), + required=False) class Meta: model = Patch fields = ['state', 'archived', 'delegate'] + widgets = { + 'state': forms.Select( + attrs={'class': 'change-property-state'}), + 'archived': forms.CheckboxInput( + attrs={'class': 'archive-patch-check'}), + } class OptionalModelChoiceField(forms.ModelChoiceField): - no_change_choice = ('*', 'no change') + no_change_choice = ('*', 'No change') to_field_name = None - def __init__(self, *args, **kwargs): + def __init__(self, *args, placeholder, className, **kwargs): + self.no_change_choice = ('*', placeholder) + self.widget = forms.Select(attrs={'class': className}) super(OptionalModelChoiceField, self).__init__( initial=self.no_change_choice[0], *args, **kwargs) @@ -166,6 +179,10 @@ class OptionalModelChoiceField(forms.ModelChoiceField): class OptionalBooleanField(forms.TypedChoiceField): + def __init__(self, className, *args, **kwargs): + self.widget = forms.Select(attrs={'class': className}) + super(OptionalBooleanField, self).__init__(*args, **kwargs) + def is_no_change(self, value): return value == self.empty_value @@ -173,17 +190,26 @@ class OptionalBooleanField(forms.TypedChoiceField): class MultiplePatchForm(forms.Form): action = 'update' archived = OptionalBooleanField( - choices=[('*', 'no change'), ('True', 'Archived'), - ('False', 'Unarchived')], + className="archive-patch-select", + choices=[('*', 'No change'), ('True', 'Archive'), + ('False', 'Unarchive')], coerce=lambda x: x == 'True', - empty_value='*') + empty_value='*', + label="Archived") def __init__(self, project, *args, **kwargs): super(MultiplePatchForm, self).__init__(*args, **kwargs) self.fields['delegate'] = OptionalModelChoiceField( - queryset=_get_delegate_qs(project=project), required=False) + queryset=_get_delegate_qs(project=project), + placeholder="Delegate to", + className="change-property-delegate", + label="Delegate to", + required=False) self.fields['state'] = OptionalModelChoiceField( - queryset=State.objects.all()) + queryset=State.objects.all(), + placeholder="Change state", + className="change-property-state", + label="Change state") def save(self, instance, commit=True): opts = instance.__class__._meta From patchwork Thu Jul 22 17:12:50 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Raxel Gutierrez X-Patchwork-Id: 1508812 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=uMFOl/A7; 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 4GVzYw28nWz9sRR for ; Fri, 23 Jul 2021 03:13:40 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4GVzYw0z3gz30L9 for ; Fri, 23 Jul 2021 03:13:40 +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=uMFOl/A7; 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=3mqf5yaukcxuktqxezhhzex.vhfitmvaphkdeblml.hsetul.hkz@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=uMFOl/A7; 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 4GVzYc2YPsz2yRX for ; Fri, 23 Jul 2021 03:13:24 +1000 (AEST) Received: by mail-qv1-xf49.google.com with SMTP id 15-20020a0562140dcfb02902e558bb7a04so22268qvt.10 for ; Thu, 22 Jul 2021 10:13:24 -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=CilxQOb6fNrQB9ly0KWrgNNcdBKn1fwsfn/b/SRH6Q8=; b=uMFOl/A7vo8+mvB5J9NHk3oGBwkTtufxRUFy81a2pew0fg1Y3UjxAOYb5qoxrPoZH8 v9mBaMgWPW2TIBJIDWTlvXwQctFig7/+tap5vG7RyW+bTshhYSzJEA83NjSR1jy6Dwcl ajCou5rAGl+KME0kl9cp4sbwGi25v/6P4IFVocZs6YREW7wkk9SPnxHcjSr/5+jKOwp4 J0qDR4JoxOaiUi+91p/4SeoyJdfPwr60cGVYygMEI7vHXCLGeASEI0cFrpJx9PWz0OJr 2gSXOZIyHFlyR69rVCc38ODgssxdSZZghad7bsIKZ99QAejSUdGDLnTeQLYqKbUWoOhu 94XQ== 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=CilxQOb6fNrQB9ly0KWrgNNcdBKn1fwsfn/b/SRH6Q8=; b=UVFiIpcUT+Wcw/GKMSrbzsU4qBpllYk+4CZ1kwxJYSfProGKjnpYNgQMrrieCBXJGl hf0rYPRTJyLzLXsyBglzQi9mNrDu3BVoEL76Uq2Xr0U3T5VCZ/TMABvM2gVU52VE9Wja 4nPz+bqofZlNmtxv3fzkDLbnBam2CPefs4BP423aFfHNeZumGZV4vi0qubuvnM5LgaMm vky2G6/Ybo8rFZaH3FK+ud+aNzn/bdWQ4+CDkxw/TADPBI8oIjafufn7I3qfq2OB0qiV 2sqAE5selbzKHqGKVMC4w5zkK0opmHvfimc69In1cmzBKaJO1BGiWmmrWPLe14WxiALT UCIA== X-Gm-Message-State: AOAM530uPLU3EW0J3ykhXgZ1kKcguVJ7dz6xAbHyDFd5iKMUnWSvCyjy rmN6qbQbtXQ6M1VdNmZxmcyygZbakGGDQ9ELlYmj6ATJD90TUhY0f/1jHyPTNWStPosNu0dCyzE 3EMuLIrJ+07evqMRXcTDnV4c6FHaPmZuZ2QwJJ9owhfDqoMGbxJY+RAp5ge0KFm/n X-Google-Smtp-Source: ABdhPJxOFdo6hS7JvwVOjUtr0XD7BZyKMo0UeLVg+0jMqyP6gVz8zbcNV2OT254oyCuQEnGPA7wm+FwjEw== X-Received: from raxel-pw.c.googlers.com ([fda3:e722:ac3:cc00:14:4d90:c0a8:2fda]) (user=raxel job=sendgmr) by 2002:a05:6214:1182:: with SMTP id t2mr971055qvv.11.1626974002167; Thu, 22 Jul 2021 10:13:22 -0700 (PDT) Date: Thu, 22 Jul 2021 17:12:50 +0000 In-Reply-To: <20210722171251.2554142-1-raxel@google.com> Message-Id: <20210722171251.2554142-5-raxel@google.com> Mime-Version: 1.0 References: <20210722171251.2554142-1-raxel@google.com> X-Mailer: git-send-email 2.32.0.432.gabb21c7263-goog Subject: [PATCH v2 4/5] static: add rest.js to handle requests & respective messages 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 js file to have REST API requests utilities JavaScript module to be reused by other Patchwork js files making updates to objects. - Add function to make fetch requests that update properties through 'PATCH' requests with the REST API endpoints. - Add functions that handle update & error messages for these 'PATCH' update requests following the Django messages framework format and form error styling. The subsequent patch will make use of these functions which will be also reused in future features the make use fetch requests to update object fields. Signed-off-by: Raxel Gutierrez --- htdocs/README.rst | 7 +++++ htdocs/js/rest.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 htdocs/js/rest.js diff --git a/htdocs/README.rst b/htdocs/README.rst index a018f31..12174de 100644 --- a/htdocs/README.rst +++ b/htdocs/README.rst @@ -138,6 +138,13 @@ js Part of Patchwork. +``rest.js.`` + + Utility module for REST API requests to be used by other Patchwork js files + (fetch requests, handling update & error messages). + + Part of Patchwork. + ``selectize.min.js`` Selectize is the hybrid of a ``textbox`` and `` + {% if not patch.delegate.username %} + + {% else %} + + {% endif %} + {% for maintainer in maintainers %} + {% if maintainer.name == patch.delegate.username %} + + {% else %} + + {% endif %} + {% endfor %} + + + + + {% empty %} diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py index 178d185..2223202 100644 --- a/patchwork/views/__init__.py +++ b/patchwork/views/__init__.py @@ -15,6 +15,7 @@ from patchwork.models import Bundle from patchwork.models import BundlePatch from patchwork.models import Patch from patchwork.models import Project +from patchwork.models import State from patchwork.models import Check from patchwork.paginator import Paginator @@ -176,6 +177,8 @@ def generic_list(request, project, view, view_args=None, filter_settings=None, 'project': project, 'projects': Project.objects.all(), 'filters': filters, + 'maintainers': project.maintainer_project.all(), + 'states': State.objects.all(), } # pagination diff --git a/templates/base.html b/templates/base.html index 8700602..e57e2d5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -114,7 +114,7 @@ {% endfor %}
{% endif %} -
+
{% block body %} {% endblock %}