]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
views: Move and refactor patch-forms
authorRaxel Gutierrez <raxel@google.com>
Mon, 23 Aug 2021 18:28:31 +0000 (18:28 +0000)
committerStephen Finucane <stephen@that.guru>
Fri, 1 Nov 2024 15:47:20 +0000 (15:47 +0000)
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 <raxel@google.com>
Signed-off-by: Stephen Finucane <stephen@that.guru>
[stephenfin: Address merge conflicts]

patchwork/forms.py
patchwork/templates/patchwork/partials/patch-forms.html [new file with mode: 0644]
patchwork/templates/patchwork/partials/patch-list.html
patchwork/templates/patchwork/submission.html
patchwork/tests/views/test_bundles.py
patchwork/views/__init__.py
patchwork/views/patch.py

index ed06d0d15070db9c2957fd120d27f3a084937fca..59e82ba047fc4914ba3a75e0bca21e96e793bf5d 100644 (file)
@@ -64,7 +64,7 @@ class BundleForm(forms.ModelForm):
         regex=r'^[^/]+$',
         min_length=1,
         max_length=50,
-        label='Name',
+        required=False,
         error_messages={'invalid': "Bundle names can't contain slashes"},
     )
 
@@ -76,12 +76,19 @@ class BundleForm(forms.ModelForm):
 class CreateBundleForm(BundleForm):
     def clean_name(self):
         name = self.cleaned_data['name']
+        if not name:
+            raise forms.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
+                '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 (file)
index 0000000..80f8281
--- /dev/null
@@ -0,0 +1,49 @@
+<div class="patch-forms" id="patch-forms">
+{% if patch_form %}
+  <div id="patch-form-properties" class="patch-form">
+    <div id="patch-form-state">
+      {{ patch_form.state.errors }}
+      {{ patch_form.state }}
+    </div>
+    <div id="patch-form-delegate">
+      {{ patch_form.delegate.errors }}
+      {{ patch_form.delegate }}
+    </div>
+    <div id="patch-form-archive">
+      {{ patch_form.archived.errors }}
+      {{ patch_form.archived.label_tag }} {{ patch_form.archived }}
+    </div>
+    <button class="patch-form-submit btn btn-primary" name="action" value="update">
+      Update
+    </button>
+  </div>
+{% endif %}
+{% if user.is_authenticated %}
+  <div id="patch-form-bundle" class="patch-form">
+    <div id="create-bundle">
+      {{ create_bundle_form.name.errors }}
+      {{ create_bundle_form.name }}
+      <input class="patch-form-submit btn btn-primary" name="action" value="Create" type="submit"/>
+    </div>
+    {% if bundles %}
+    <div id="add-to-bundle">
+      <select class="add-bundle" name="bundle_id">
+      <option value="" selected>Add to bundle</option>
+      {% for bundle in bundles %}
+        <option value="{{bundle.id}}">{{bundle.name}}</option>
+      {% endfor %}
+      </select>
+      <input class="patch-form-submit btn btn-primary" name="action" value="Add" type="submit"/>
+    </div>
+    {% endif %}
+    {% if bundle %}
+    <div id="remove-bundle">
+      <input type="hidden" name="removed_bundle_id" value="{{bundle.id}}"/>
+      <button class="patch-form-submit btn btn-primary" name="action" value="Remove">
+        Remove from bundle
+      </button>
+    </div>
+    {% endif %}
+  </div>
+{% endif %}
+</div>
index 1bdef36d23b03747a374befc91467efdccd27f87..981ceee58d2c4ad0f28c199e3c82a2118f5b8b27 100644 (file)
@@ -9,7 +9,6 @@
 {% endblock %}
 
 {% include "patchwork/partials/filters.html" %}
-{% include "patchwork/partials/pagination.html" %}
 
 {% if order.editable %}
 <table class="patch-list">
 </table>
 {% endif %}
 
-{% if page.paginator.long_page and user.is_authenticated %}
-<div class="floaty">
-  <a title="jump to form" href="#patch-forms">
-    <span style="font-size: 120%">&#9662;</span>
-  </a>
-</div>
-{% endif %}
-
-<form method="post">
+<form id="patch-list-form" method="post">
   {% csrf_token %}
   <input type="hidden" name="form" value="patch-list-form"/>
   <input type="hidden" name="project" value="{{project.id}}"/>
+
+  <div class="patch-list-actions">
+{% include "patchwork/partials/patch-forms.html" %}
+{% include "patchwork/partials/pagination.html" %}
+  </div>
   <table id="patch-list" class="table table-hover table-extra-condensed table-striped pw-list" data-toggle="checkboxes" data-range="true">
     <thead>
       <tr>
 
     <tbody>
 {% for patch in page.object_list %}
-      <tr id="patch-row:{{patch.id}}">
+      <tr id="patch-row:{{patch.id}}" data-patch-id="{{patch.id}}">
 {% if user.is_authenticated %}
         <td id="select-patch:{{patch.id}}" style="text-align: center;">
           <input type="checkbox" name="patch_id:{{patch.id}}"/>
 
 {% if page.paginator.count %}
 {% include "patchwork/partials/pagination.html" %}
-
-  <div class="patch-forms" id="patch-forms">
-
-{% if patch_form %}
-     <div class="patch-form patch-form-properties">
-       <h3>Properties</h3>
-       <table class="form">
-         <tr>
-           <th>Change state:</th>
-           <td>
-             {{ patch_form.state }}
-             {{ patch_form.state.errors }}
-           </td>
-         </tr>
-         <tr>
-           <th>Delegate to:</th>
-           <td>
-             {{ patch_form.delegate }}
-             {{ patch_form.delegate.errors }}
-           </td>
-         </tr>
-         <tr>
-           <th>Archive:</th>
-           <td>
-             {{ patch_form.archived }}
-             {{ patch_form.archived.errors }}
-           </td>
-         </tr>
-         <tr>
-           <td></td>
-           <td>
-             <input type="submit" name="action" value="{{patch_form.action}}"/>
-           </td>
-         </tr>
-       </table>
-     </div>
-{% endif %}
-
-{% if user.is_authenticated %}
-    <div class="patch-form patch-form-bundle">
-      <h3>Bundling</h3>
-      <table class="form">
-        <tr>
-         <td>Create bundle:</td>
-         <td>
-           <input type="text" name="bundle_name"/>
-           <input name="action" value="Create" type="submit"/>
-         </td>
-        </tr>
-{% if bundles %}
-        <tr>
-          <td>Add to bundle:</td>
-          <td>
-            <select name="bundle_id">
-{% for bundle in bundles %}
-              <option value="{{bundle.id}}">{{bundle.name}}</option>
-{% endfor %}
-            </select>
-            <input name="action" value="Add" type="submit"/>
-          </td>
-        </tr>
-{% endif %}
-{% if bundle %}
-        <tr>
-          <td>Remove from bundle:</td>
-          <td>
-            <input type="hidden" name="removed_bundle_id" value="{{bundle.id}}"/>
-            <input name="action" value="Remove" type="submit"/>
-          </td>
-        </tr>
-{% endif %}
-      </table>
-    </div>
-{% endif %}
-    <div style="clear: both;">
-    </div>
-  </div>
 {% endif %}
 </form>
index aa4e5553dc195c435f7a3b2ea4f8c58295a39f71..cd74491c0e92c1896f2a6e885296414d75bf8495 100644 (file)
 {% endif %}
 </table>
 
-<div class="patch-forms">
-{% if patch_form %}
-  <div class="patch-form patch-form-properties">
-    <h3>Patch Properties</h3>
-    <form method="post">
-      {% csrf_token %}
-      <table class="form">
-        <tr>
-          <th>Change state:</th>
-          <td>
-            {{ patch_form.state }}
-            {{ patch_form.state.errors }}
-          </td>
-        </tr>
-        <tr>
-          <th>Delegate to:</th>
-          <td>
-            {{ patch_form.delegate }}
-            {{ patch_form.delegate.errors }}
-          </td>
-        </tr>
-        <tr>
-          <th>Archived:</th>
-          <td>
-            {{ patch_form.archived }}
-            {{ patch_form.archived.errors }}
-          </td>
-        </tr>
-        <tr>
-          <td></td>
-          <td>
-            <input type="submit" value="Update">
-          </td>
-        </tr>
-      </table>
-    </form>
-  </div>
-{% endif %}
-
-{% if create_bundle_form %}
-  <div class="patch-form patch-form-bundle">
-    <h3>Bundling</h3>
-    <table class="form">
-      <tr>
-        <td>Create bundle:</td>
-        <td>
-{% if create_bundle_form.non_field_errors %}
-          <dd class="errors">{{create_bundle_form.non_field_errors}}</dd>
-{% endif %}
-          <form method="post">
-            {% csrf_token %}
-            <input type="hidden" name="action" value="createbundle"/>
-{% if create_bundle_form.name.errors %}
-            <dd class="errors">{{create_bundle_form.name.errors}}</dd>
-{% endif %}
-            {{ create_bundle_form.name }}
-            <input value="Create" type="submit"/>
-          </form>
-        </td>
-      </tr>
-{% if bundles %}
-      <tr>
-        <td>Add to bundle:</td>
-        <td>
-          <form method="post">
-{% csrf_token %}
-            <input type="hidden" name="action" value="addtobundle"/>
-            <select name="bundle_id"/>
-{% for bundle in bundles %}
-              <option value="{{bundle.id}}">{{bundle.name}}</option>
-{% endfor %}
-            </select>
-            <input value="Add" type="submit"/>
-          </form>
-        </td>
-      </tr>
-{% endif %}
-    </table>
-  </div>
-{% endif %}
-  <div style="clear: both;">
-  </div>
-</div>
+<form id="patch-list-form" method="POST">
+  {% csrf_token %}
+  {% include "patchwork/partials/patch-forms.html" %}
+</form>
 
 {% if submission.pull_url %}
 <h2>Pull-request</h2>
index 24ebe6123a994ca3b43751bb1aaa952f91740551..1d317c3059c7312e68fdbe09f88d985432343e1d 100644 (file)
@@ -369,7 +369,7 @@ class BundleCreateFromListTest(BundleTestBase):
         newbundlename = 'testbundle-new'
         params = {
             'form': 'patch-list-form',
-            'bundle_name': newbundlename,
+            'name': newbundlename,
             'action': 'Create',
             'project': self.project.id,
         }
@@ -389,7 +389,7 @@ class BundleCreateFromListTest(BundleTestBase):
 
         params = {
             'form': 'patch-list-form',
-            'bundle_name': newbundlename,
+            'name': newbundlename,
             'action': 'Create',
             'project': self.project.id,
             'patch_id:%d' % patch.id: 'checked',
@@ -418,7 +418,7 @@ class BundleCreateFromListTest(BundleTestBase):
 
         params = {
             'form': 'patch-list-form',
-            'bundle_name': '',
+            'name': '',
             'action': 'Create',
             'project': self.project.id,
             'patch_id:%d' % patch.id: 'checked',
@@ -444,7 +444,7 @@ class BundleCreateFromListTest(BundleTestBase):
 
         params = {
             'form': 'patch-list-form',
-            'bundle_name': newbundlename,
+            'name': newbundlename,
             'action': 'Create',
             'project': self.project.id,
             'patch_id:%d' % patch.id: 'checked',
@@ -475,7 +475,9 @@ class BundleCreateFromListTest(BundleTestBase):
         )
 
         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)
 
@@ -485,7 +487,7 @@ class BundleCreateFromPatchTest(BundleTestBase):
         newbundlename = 'testbundle-new'
         patch = self.patches[0]
 
-        params = {'name': newbundlename, 'action': 'createbundle'}
+        params = {'name': newbundlename, 'action': 'Create'}
 
         response = self.client.post(
             reverse(
@@ -508,7 +510,7 @@ class BundleCreateFromPatchTest(BundleTestBase):
         newbundlename = self.bundle.name
         patch = self.patches[0]
 
-        params = {'name': newbundlename, 'action': 'createbundle'}
+        params = {'name': newbundlename, 'action': 'Create'}
 
         response = self.client.post(
             reverse(
@@ -644,7 +646,7 @@ class BundleAddFromListTest(BundleTestBase):
 class BundleAddFromPatchTest(BundleTestBase):
     def test_add_to_empty_bundle(self):
         patch = self.patches[0]
-        params = {'action': 'addtobundle', 'bundle_id': self.bundle.id}
+        params = {'action': 'Add', 'bundle_id': self.bundle.id}
 
         response = self.client.post(
             reverse(
@@ -659,7 +661,7 @@ class BundleAddFromPatchTest(BundleTestBase):
 
         self.assertContains(
             response,
-            'added to bundle &quot;%s&quot;' % self.bundle.name,
+            'added to bundle %s' % self.bundle.name,
             count=1,
         )
 
@@ -669,7 +671,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', 'bundle_id': self.bundle.id}
+        params = {'action': 'Add', 'bundle_id': self.bundle.id}
 
         response = self.client.post(
             reverse(
@@ -684,7 +686,7 @@ class BundleAddFromPatchTest(BundleTestBase):
 
         self.assertContains(
             response,
-            'added to bundle &quot;%s&quot;' % self.bundle.name,
+            'added to bundle %s' % self.bundle.name,
             count=1,
         )
 
@@ -724,7 +726,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,
         }
index 51649488f68ba33a9670e6a258b09dd876cf3d99..db484c79960d0e645496633e2481df42e0aab2a5 100644 (file)
@@ -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,52 +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()
@@ -165,10 +150,26 @@ def set_bundle(request, project, action, data, patches, context):
                     "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(
@@ -232,6 +233,7 @@ def generic_list(
         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
@@ -241,19 +243,20 @@ def generic_list(
             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(
@@ -320,6 +323,7 @@ def generic_list(
         {
             'page': paginator.current_page,
             'patch_form': properties_form,
+            'create_bundle_form': create_bundle_form,
             'project': project,
             'order': order,
         }
index 838391635375d396d92e3d19724f020fb9971537..efe94f17c942fd8a2d3eceb4cc085e2d0164ce0d 100644 (file)
@@ -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
 
@@ -64,6 +64,7 @@ def patch_detail(request, project_id, msgid):
 
     form = None
     create_bundle_form = None
+    errors = None
 
     if editable:
         form = PatchForm(instance=patch)
@@ -75,39 +76,15 @@ def patch_detail(request, project_id, msgid):
         if action:
             action = action.lower()
 
-        if action == 'createbundle':
-            bundle = Bundle(owner=request.user, project=project)
-            create_bundle_form = CreateBundleForm(
-                instance=bundle, data=request.POST
+        if action in ['create', 'add']:
+            errors = set_bundle(
+                request, project, action, request.POST, [patch]
             )
-            if create_bundle_form.is_valid():
-                create_bundle_form.save()
-                bundle.append_patch(patch)
-                bundle.save()
-                create_bundle_form = 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
         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()
@@ -147,6 +124,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)