[PATCH v3 0/5] patch-list: improve usability of list action bar

Daniel Axtens dja at axtens.net
Tue Aug 3 11:59:36 AEST 2021


Hi Raxel,

A few bugs:

 - if you submit a change with the dropdowns that errors out, and then
   subsequently submit one that succeeds, the error messages are not
   cleared.

 - You've somehow ended up with two td.patch-delegate:

   <td id="patch-delegate:20">
    <td id="patch-delegate:20">
      
        
      
     </td>
     <td id="patch-state:20">
      
        New
      
     </td>
  </tr>

  This is throwing off column alignment: delegates now line up under the
  State heading.

> For the first patch, the recently released v3.0.0 of the JS cookie 
> library replaces the previous version. 

 - the js.cookie.min.js file is still corrupted and I redownloaded it
   from a CDN. I'm not sure where the bug lies here and it's probably
   not something you need to fix, but a link to the source in the commit
   message would be nice.

> For the second patch, following jrnieder’s comments [2], the commit 
> message is updated to better explain the benefits of the change and its
> details. Also, an unclear comment in forms.py for BundleForm is cleaned
> up to better explain what the changes do. The id for <td> table cells in
> patch-list.html are correctly differentiated by patch id. I noticed a 
> bug where property changes in the patch detail page weren’t working
> because the respective “update” action was not accounted for.  
>

This (patch 2) is where I'm up to in reviews atm. I'm hoping to do more soon.

> For the third patch, the “jump to form” arrow is removed [3].
>
> For the fourth patch, the `updateProperty` function now returns whether
> the update request was successful or not with `response.ok` so that 
> callers can use that information and respond accordingly. The next patch
> adds a use case for the return value.
>
> For the fifth patch, the inline dropdowns are changed so that they are 
> only visible to logged in users. Also, upon any unsuccessful update
> requests, the dropdown selection reverts to its previous selection.
> Since update requests to a patch’s state and delegate fields fail for
> unauthorized users, this behavior covers the case where unauthorized
> users don't see the current state of the db after they change the
> dropdown selection. This behavior is a useful example of patch four’s
> change of returning whether an update request is successful.

 - they're visible to logged in users but still suggest that if I am a
   regular user that I have the ability to change anyone's patch
   state/delegate. I am still a bit dubious overall about inline
   dropdowns in general, although I am cognisant that it's much more
   discoverable this way. Either way, IMO dropdowns need to be
   restricted to editable patches.

Kind regards,
Daniel

> [1] https://lists.ozlabs.org/pipermail/patchwork/2021-July/006968.html
> [2] https://lists.ozlabs.org/pipermail/patchwork/2021-July/006988.html
> [3] https://lists.ozlabs.org/pipermail/patchwork/2021-July/006991.html
>
> Raxel Gutierrez (5):
>   static: add JS Cookie Library to get csrftoken for fetch requests
>   patch-list: clean up patch-list page and refactor patch forms
>   patch-list: style modification forms as an action bar
>   static: add rest.js to handle requests & respective messages
>   patch-list: add inline dropdown for delegate and state one-off changes
>
>  htdocs/README.rst                             |  23 +++
>  htdocs/css/style.css                          |  85 +++++++--
>  htdocs/js/js.cookie.min.js                    |   2 +
>  htdocs/js/patch-list.js                       |  60 ++++++
>  htdocs/js/rest.js                             |  72 ++++++++
>  patchwork/forms.py                            |  66 +++++--
>  patchwork/templates/patchwork/list.html       |  11 +-
>  .../templates/patchwork/partials/errors.html  |   9 +
>  .../patchwork/partials/patch-forms.html       |  45 +++++
>  .../patchwork/partials/patch-list.html        | 171 ++++++------------
>  patchwork/templates/patchwork/submission.html |  91 +---------
>  patchwork/tests/views/test_bundles.py         |  44 ++---
>  patchwork/tests/views/test_patch.py           |   4 +-
>  patchwork/views/__init__.py                   |  76 ++++----
>  patchwork/views/patch.py                      |  35 +---
>  templates/base.html                           |   3 +-
>  16 files changed, 472 insertions(+), 325 deletions(-)
>  create mode 100644 htdocs/js/js.cookie.min.js
>  create mode 100644 htdocs/js/patch-list.js
>  create mode 100644 htdocs/js/rest.js
>  create mode 100644 patchwork/templates/patchwork/partials/errors.html
>  create mode 100644 patchwork/templates/patchwork/partials/patch-forms.html
>
> Range-diff:
> 1:  2a34394 ! 1:  859789e static: add JS Cookie Library to get csrftoken for fetch requests
>     @@ htdocs/README.rst: js
>      +  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
>     ++  :Version: 3.0.0
>      +
>       ``selectize.min.js``
>       
>     @@ htdocs/README.rst: js
>      
>       ## htdocs/js/js.cookie.min.js (new) ##
>      @@
>     -+/*! js-cookie v2.2.1 | MIT */
>     -+
>     -+!function(a){var b;if("function"==typeof define&&define.amd&&(define(a),b=!0),"object"==typeof exports&&(module.exports=a(),b=!0),!b){var c=window.Cookies,d=window.Cookies=a();d.noConflict=function(){return window.Cookies=c,d}}}(function(){function a(){for(var a=0,b={};a<arguments.length;a++){var c=arguments[a];for(var d in c)b[d]=c[d]}return b}function b(a){return a.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent)}function c(d){function e(){}function f(b,c,f){if("undefined"!=typeof document){f=a({path:"/"},e.defaults,f),"number"==typeof f.expires&&(f.expires=new Date(1*new Date+864e5*f.expires)),f.expires=f.expires?f.expires.toUTCString():"";try{var g=JSON.stringify(c);/^[\{\[]/.test(g)&&(c=g)}catch(j){}c=d.write?d.write(c,b):encodeURIComponent(c+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(b+"").replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[\(\)]/g,escape);var h="";for(var i in f)f[i]&&(h+="; "+i,!0!==f[i]&&(h+="="+f[i].split(";")[0]));return document.cookie=b+"="+c+h}}function g(a,c){if("undefined"!=typeof document){for(var e={},f=document.cookie?document.cookie.split("; "):[],g=0;g<f.length;g++){var h=f[g].split("="),i=h.slice(1).join("=");c||'"'!==i.charAt(0)||(i=i.slice(1,-1));try{var j=b(h[0]);if(i=(d.read||d)(i,j)||b(i),c)try{i=JSON.parse(i)}catch(k){}if(e[j]=i,a===j)break}catch(k){}}return a?e[a]:e}}return e.set=f,e.get=function(a){return g(a,!1)},e.getJSON=function(a){return g(a,!0)},e.remove=function(b,c){f(b,"",a(c,{expires:-1}))},e.defaults={},e.withConverter=c,e}return c(function(){})});
>     - \ No newline at end of file
>     ++/*! js-cookie v3.0.0 | MIT */
>     ++!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,r=e.Cookies=t();r.noConflict=function(){return e.Cookies=n,r}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)e[r]=n[r]}return e}var t={read:function(e){return e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}};return function n(r,o){function i(t,n,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape),n=r.write(n,t);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n+c}}return Object.create({set:i,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var n=document.cookie?document.cookie.split("; "):[],o={},i=0;i<n.length;i++){var c=n[i].split("="),u=c.slice(1).join("=");'"'===u[0]&&(u=u.slice(1,-1));try{var f=t.read(c[0]);if(o[f]=r.read(u,f),e===f)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){i(t,"",e({},n,{expires:-1}))},withAttributes:function(t){return n(this.converter,e({},this.attributes,t))},withConverter:function(t){return n(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(r)}})}(t,{path:"/"})}));
>      
>       ## templates/base.html ##
>      @@
> 2:  b30ba35 ! 2:  2ac10c8 patch-list: clean up patch-list page and refactor patch forms
>     @@ Commit message
>          patch-list: clean up patch-list page and refactor patch forms
>      
>          Clean up the patch-list page by moving forms to a new template file
>     -    patch-forms.html and moving them to the top of the page, adding ids to
>     -    table cells, and renaming selectors using hyphen delimited strings where
>     -    the relevant changes were made. Also, created a partial template for
>     +    patch-forms.html and move them to the top of the page, add ids to
>     +    table cells, and rename selectors using hyphen delimited strings where
>     +    the relevant changes were made. Also, create a partial template for
>          errors that render with form submission. These changes improve the
>     -    discoverability of the patch-list forms and makes the code healthier,
>     +    discoverability of the patch-list forms and make the code healthier,
>          ready for change, and overall more readable.
>      
>          Also, move patch-list related js code to a new patch-list.js file, to
>     @@ Commit message
>          simplifies a possible future migration to TypeScript if the project
>          chooses to go in that direction.
>      
>     -    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. Error messages and success/warning
>     -    messages are now normalized throughout these files for testing and
>     -    functionality. Also, important to these change is that CreateBundleForm
>     -    in forms.py handles the validation of the bundle form input so that now
>     -    the patch-list bundle form also makes use of the Django forms API.
>     +    No user-visible change should be noticed since this change does not
>     +    make stye changes. 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 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.
>      
>       ## htdocs/README.rst ##
>      @@ htdocs/README.rst: js
>         :GitHub: https://github.com/js-cookie/js-cookie/
>     -   :Version: 2.2.1
>     +   :Version: 3.0.0
>       
>      +``patch-list.js.``
>      +
>     -+  Utility functions for patch list manipulation (inline dropdown changes,
>     -+  etc.)
>     ++  Event helpers and other application logic for patch-list.html. These
>     ++  support patch list manipulation (e.g. inline dropdown changes).
>      +
>      +  Part of Patchwork.
>      +
>     @@ htdocs/js/patch-list.js (new)
>      +        e.preventDefault();
>      +    });
>      +});
>     - \ No newline at end of file
>      
>       ## patchwork/forms.py ##
>      @@
>     @@ patchwork/forms.py: class EmailForm(forms.Form):
>       
>       
>       class BundleForm(forms.ModelForm):
>     -+    # Map 'name' field to 'bundle_name' attr
>      +    field_mapping = {'name': 'bundle_name'}
>           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'})
>       
>     -+    # Maps form fields 'name' attr rendered in HTML element
>     ++    # Changes the HTML 'name' attr of the input element from "name"
>     ++    # (inherited from the model field 'name' of the Bundle object)
>     ++    # to "bundle_name" as the server expects "bundle_name" as a
>     ++    # parameter not "name" to process patch forms 'POST' requests.
>      +    def add_prefix(self, field_name):
>      +        field_name = self.field_mapping.get(field_name, field_name)
>      +        return super(BundleForm, self).add_prefix(field_name)
>     @@ patchwork/templates/patchwork/partials/patch-list.html
>       {% if order.editable %}
>       <table class="patchlist">
>      @@
>     - 
>     - {% if page.paginator.long_page and user.is_authenticated %}
>     - <div class="floaty">
>     -- <a title="jump to form" href="#patchforms">
>     -+ <a title="jump to form" href="#patch-forms">
>     -   <span style="font-size: 120%">▾</span>
>     -  </a>
>     - </div>
>     + </table>
>       {% endif %}
>       
>     +-{% if page.paginator.long_page and user.is_authenticated %}
>     +-<div class="floaty">
>     +- <a title="jump to form" href="#patchforms">
>     +-  <span style="font-size: 120%">▾</span>
>     +- </a>
>     +-</div>
>     +-{% endif %}
>     +-
>      -<script type="text/javascript">
>      -$(document).ready(function() {
>      -    $('#patchlist').stickyTableHeaders();
>     @@ patchwork/templates/patchwork/partials/patch-list.html: $(document).ready(functi
>      +  <tr id="patch_row:{{patch.id}}" data-patch-id="{{patch.id}}">
>          {% if user.is_authenticated %}
>      -   <td style="text-align: center;">
>     -+   <td id="select-patch" style="text-align: center;">
>     ++   <td id="select-patch:{{patch.id}}" style="text-align: center;">
>           <input type="checkbox" name="patch_id:{{patch.id}}"/>
>          </td>
>          {% endif %}
>     @@ patchwork/templates/patchwork/partials/patch-list.html: $(document).ready(functi
>          </td>
>          {% endif %}
>      -   <td>
>     -+   <td id="patch-name">
>     ++   <td id="patch-name:{{patch.id}}">
>           <a href="{% url 'patch-detail' project_id=project.linkname msgid=patch.url_msgid %}">
>            {{ patch.name|default:"[no subject]"|truncatechars:100 }}
>           </a>
>          </td>
>      -   <td>
>     -+   <td id="patch-series">
>     ++   <td id="patch-series:{{patch.id}}">
>           {% if patch.series %}
>           <a href="?series={{patch.series.id}}">
>            {{ patch.series|truncatechars:100 }}
>     @@ patchwork/templates/patchwork/partials/patch-list.html: $(document).ready(functi
>      -   <td>{{ patch.submitter|personify:project }}</td>
>      -   <td>{{ patch.delegate.username }}</td>
>      -   <td>{{ patch.state }}</td>
>     -+   <td id="patch-tags" class="text-nowrap">{{ patch|patch_tags }}</td>
>     -+   <td id="patch-checks" class="text-nowrap">{{ patch|patch_checks }}</td>
>     -+   <td id="patch-date" class="text-nowrap">{{ patch.date|date:"Y-m-d" }}</td>
>     -+   <td id="patch-submitter">{{ patch.submitter|personify:project }}</td>
>     -+   <td id="patch-delegate">{{ patch.delegate.username }}</td>
>     -+   <td id="patch-state">{{ patch.state }}</td>
>     ++   <td id="patch-tags:{{patch.id}}" class="text-nowrap">{{ patch|patch_tags }}</td>
>     ++   <td id="patch-checks:{{patch.id}}" class="text-nowrap">{{ patch|patch_checks }}</td>
>     ++   <td id="patch-date:{{patch.id}}" class="text-nowrap">{{ patch.date|date:"Y-m-d" }}</td>
>     ++   <td id="patch-submitter:{{patch.id}}">{{ patch.submitter|personify:project }}</td>
>     ++   <td id="patch-delegate:{{patch.id}}">{{ patch.delegate.username }}</td>
>     ++   <td id="patch-state:{{patch.id}}">{{ patch.state }}</td>
>         </tr>
>        {% empty %}
>         <tr>
>     @@ patchwork/tests/views/test_patch.py: class PatchUpdateTest(TestCase):
>      
>       ## patchwork/views/__init__.py ##
>      @@
>     - # Copyright (C) 2008 Jeremy Kerr <jk at ozlabs.org>
>       #
>       # SPDX-License-Identifier: GPL-2.0-or-later
>     -+import json
>       
>     ++import json
>     ++
>       from django.contrib import messages
>       from django.shortcuts import get_object_or_404
>     -@@ patchwork/views/__init__.py: from django.db.models import Prefetch
>     + from django.db.models import Prefetch
>       
>       from patchwork.filters import Filters
>     - from patchwork.forms import MultiplePatchForm
>      +from patchwork.forms import CreateBundleForm
>     + from patchwork.forms import MultiplePatchForm
>       from patchwork.models import Bundle
>       from patchwork.models import BundlePatch
>     - from patchwork.models import Patch
>     -@@ patchwork/views/__init__.py: from patchwork.models import Project
>     - from patchwork.models import Check
>     - from patchwork.paginator import Paginator
>     - 
>     --
>     - bundle_actions = ['create', 'add', 'remove']
>     - 
>     - 
>      @@ patchwork/views/__init__.py: class Order(object):
>       
>       
>     @@ patchwork/views/__init__.py: class Order(object):
>      +            messages.success(request, 'Bundle %s created' % bundle.name)
>      +        else:
>      +            formErrors = json.loads(create_bundle_form.errors.as_json())
>     -+            errors = [formErrors['name'][0]['message']]
>     ++            errors = [e['message'] for e in formErrors['name']]
>      +            return errors
>           elif action == 'add':
>      +        if not data['bundle_id']:
>     @@ patchwork/views/patch.py: def patch_detail(request, project_id, msgid):
>               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()
>      @@ patchwork/views/patch.py: def patch_detail(request, project_id, msgid):
>           context['project'] = patch.project
>           context['related_same_project'] = related_same_project
> 3:  3571476 ! 3:  5d5eedc patch-list: style modification forms as an action bar
>     @@ patchwork/forms.py: class BundleForm(forms.ModelForm):
>      +            attrs={'class': 'create-bundle',
>      +                   'placeholder': 'Bundle name'}))
>       
>     -     # Maps form fields 'name' attr rendered in HTML element
>     -     def add_prefix(self, field_name):
>     +     # Changes the HTML 'name' attr of the input element from "name"
>     +     # (inherited from the model field 'name' of the Bundle object)
>      @@ patchwork/forms.py: class PatchForm(forms.ModelForm):
>           def __init__(self, instance=None, project=None, *args, **kwargs):
>               super(PatchForm, self).__init__(instance=instance, *args, **kwargs)
> 4:  c3f2a17 ! 4:  d77051d static: add rest.js to handle requests & respective messages
>     @@ htdocs/js/rest.js (new)
>      +        body: JSON.stringify(data),
>      +    });
>      +
>     -+    await fetch(request)
>     ++    return await fetch(request)
>      +        .then(response => {
>      +            if (!response.ok) {
>      +                response.text().then(text => {
>     @@ htdocs/js/rest.js (new)
>      +                // Update message for successful changes
>      +                handleUpdateMessages(updateMessage.some);
>      +            }
>     ++            return response.ok
>      +        });
>      +}
>      +
> 5:  62e3434 ! 5:  e4e0674 patch-list: add inline dropdown for delegate and state one-off changes
>     @@ Commit message
>          patch-list: add inline dropdown for delegate and state one-off changes
>      
>          Add dropdown for the cell values of the Delegate and State columns for
>     -    each individual patch to make one-off changes to patches. Change the
>     -    generic_list method to pass the list of states and maintainers to the
>     -    patch list view context to populate the dropdown options. The static
>     -    patch-list.js file now uses the modularity of the fetch request and
>     -    update/error messages handling of rest.js.
>     +    each individual patch to make one-off changes to patches. The dropdowns
>     +    are only viewable when logged in and revert selections made by users
>     +    that don't have permission to change a given patch's state or delegate.
>     +    Change the generic_list method to pass the list of states and
>     +    maintainers to the patch list view context to populate the dropdown
>     +    options. The static patch-list.js file now uses the modularity of the
>     +    fetch request and update/error messages handling of rest.js.
>      
>       ## htdocs/js/patch-list.js ##
>      @@
>      +import { updateProperty } from "./rest.js";
>      +
>       $( document ).ready(function() {
>     ++    let inlinePropertyDropdowns = $("td > select[class^='change-property-']");
>     ++    $(inlinePropertyDropdowns).each(function() {
>     ++        // Store previous dropdown selection
>     ++        $(this).data("prevProperty", $(this).val());
>     ++    });
>     ++
>      +    // Change listener for dropdowns that change an individual patch's delegate and state properties
>     -+    $("select[class^='change-property-']").change((event) => {
>     ++    $(inlinePropertyDropdowns).change((event) => {
>      +        const property = event.target.getAttribute("value");
>      +        const { url, data } = getPatchProperties(event.target, property);
>      +        const updateMessage = {
>      +            'none': "No patches updated",
>      +            'some': "1 patch updated",
>      +        };
>     -+        updateProperty(url, data, updateMessage);
>     ++        updateProperty(url, data, updateMessage).then(is_success => {
>     ++            if (!is_success) {
>     ++                // Revert to previous selection
>     ++                $(event.target).val($(event.target).data("prevProperty"));
>     ++            } else {
>     ++                // Update to new previous selection
>     ++                $(event.target).data("prevProperty", $(event.target).val());
>     ++            }
>     ++        });
>      +    });
>      +
>           $("#patchlist").stickyTableHeaders();
>     @@ htdocs/js/patch-list.js: $( document ).ready(function() {
>      +        };
>      +    }
>       });
>     - \ No newline at end of file
>      
>       ## htdocs/js/rest.js ##
>      @@ htdocs/js/rest.js: function handleErrorMessages(errorMessage) {
>     @@ patchwork/templates/patchwork/partials/patch-list.html
>       
>       {% include "patchwork/partials/filters.html" %}
>      @@
>     -    <td id="patch-checks" class="text-nowrap">{{ patch|patch_checks }}</td>
>     -    <td id="patch-date" class="text-nowrap">{{ patch.date|date:"Y-m-d" }}</td>
>     -    <td id="patch-submitter">{{ patch.submitter|personify:project }}</td>
>     --   <td id="patch-delegate">{{ patch.delegate.username }}</td>
>     --   <td id="patch-state">{{ patch.state }}</td>
>     -+   <td id="patch-delegate">
>     -+      <select class="change-property-delegate" value="delegate">
>     -+        {% if not patch.delegate.username %}
>     -+          <option value="*" selected>No delegate</option>
>     -+        {% else %}
>     -+          <option value="*">No delegate</option>
>     -+        {% endif %}
>     -+        {% for maintainer in maintainers %}
>     -+          {% if maintainer.name == patch.delegate.username %}
>     -+            <option value="{{ patch.delegate.username.id }}" selected>{{ patch.delegate.username }}</option>
>     +    <td id="patch-checks:{{patch.id}}" class="text-nowrap">{{ patch|patch_checks }}</td>
>     +    <td id="patch-date:{{patch.id}}" class="text-nowrap">{{ patch.date|date:"Y-m-d" }}</td>
>     +    <td id="patch-submitter:{{patch.id}}">{{ patch.submitter|personify:project }}</td>
>     +-   <td id="patch-delegate:{{patch.id}}">{{ patch.delegate.username }}</td>
>     +-   <td id="patch-state:{{patch.id}}">{{ patch.state }}</td>
>     ++   <td id="patch-delegate:{{patch.id}}">
>     ++    <td id="patch-delegate:{{patch.id}}">
>     ++      {% if user.is_authenticated %}
>     ++        <select class="change-property-delegate" value="delegate">
>     ++          {% if not patch.delegate.username %}
>     ++            <option value="*" selected>No delegate</option>
>      +          {% else %}
>     -+            <option value="{{ maintainer.id }}">{{ maintainer.name }}</option>
>     ++            <option value="*">No delegate</option>
>      +          {% endif %}
>     -+        {% endfor %}
>     -+      </select>
>     -+    </td>
>     -+   <td id="patch-state">
>     -+    <select class="change-property-state" value="state">
>     -+      {% for state in states %}
>     -+        {% if state.name == patch.state.name %}
>     -+          <option value="{{ patch.state.ordering }}" selected>{{ patch.state }}</option>
>     -+        {% else %}
>     -+          <option value="{{ state.ordering  }}">{{ state.name }}</option>
>     -+        {% endif %}
>     -+      {% endfor %}
>     -+    </select>
>     -+  </td>
>     ++          {% for maintainer in maintainers %}
>     ++            {% if maintainer.name == patch.delegate.username %}
>     ++              <option value="{{ patch.delegate.username.id }}" selected>{{ patch.delegate.username }}</option>
>     ++            {% else %}
>     ++              <option value="{{ maintainer.id }}">{{ maintainer.name }}</option>
>     ++            {% endif %}
>     ++          {% endfor %}
>     ++        </select>
>     ++      {% else %}
>     ++        {{ patch.delegate.username }}
>     ++      {% endif %}
>     ++     </td>
>     ++     <td id="patch-state:{{patch.id}}">
>     ++      {% if user.is_authenticated %}
>     ++        <select class="change-property-state" value="state">
>     ++          {% for state in states %}
>     ++            {% if state.name == patch.state.name %}
>     ++              <option value="{{ patch.state.ordering }}" selected>{{ patch.state }}</option>
>     ++            {% else %}
>     ++              <option value="{{ state.ordering  }}">{{ state.name }}</option>
>     ++            {% endif %}
>     ++          {% endfor %}
>     ++        </select>
>     ++      {% else %}
>     ++        {{ patch.state }}
>     ++      {% endif %}
>     ++     </td>
>         </tr>
>        {% empty %}
>         <tr>
> -- 
> 2.32.0.554.ge1b32706d8-goog
>
> _______________________________________________
> Patchwork mailing list
> Patchwork at lists.ozlabs.org
> https://lists.ozlabs.org/listinfo/patchwork


More information about the Patchwork mailing list