Repository: Ouranosinc/Magpie

Issue:
[BUG] Rendering UI improvements
## Describe the bug

Some places where rendering of UI views can be improved.

## Screenshots

Unnecessary line (both group and user views) 
![image](https://user-images.githubusercontent.com/19194484/118888178-50e42b80-b8c9-11eb-86fe-2fbdf873a166.png)

Unnecessary "Apply Permissions" button when none can actually be applied (no corresponding services for the type) 
![image](https://user-images.githubusercontent.com/19194484/118888100-327e3000-b8c9-11eb-9227-8908050a68d0.png)

Checkbox for discoverable group status 
(although disabled for not applicable groups, it would be better to replace it altogether by similar method as user lock icon)
![image](https://user-images.githubusercontent.com/19194484/118888387-97398a80-b8c9-11eb-8659-25350a3238e4.png)
![image](https://user-images.githubusercontent.com/19194484/118888367-8f79e600-b8c9-11eb-888f-d59466605b51.png)


## Environment
 - Browser: any
 - Instance: https://host-140-46.rdext.crim.ca/magpie
 - Version 3.12.0

Discussion:

Patch:
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 4fa193fd..4f2190ef 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -11,8 +11,9 @@ formats:
   #- pdf
   #- epub
 python:
-  version: 3.7
+  version: "3.7"
   install:
+    - requirements: requirements-sys.txt
     - requirements: requirements-doc.txt
     # required for OpenAPI generation
     - requirements: requirements.txt
diff --git a/CHANGES.rst b/CHANGES.rst
index cb4dcaad..ff55ae7d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,7 +7,21 @@ Changes
 `Unreleased <https://github.com/Ouranosinc/Magpie/tree/master>`_ (latest)
 ------------------------------------------------------------------------------------
 
-* Nothing new for the moment.
+Features / Changes
+~~~~~~~~~~~~~~~~~~~~~
+* Add improved UI display of long ``Permission`` titles for ``Resource`` hierarchy tree headers.
+* Add improved UI display of ``Permission`` titles using capitalization for better readability.
+* Add UI display of the ``Resource`` type as label before its name for each item within the hierarchy tree.
+* Add UI display of lock icon and message for special ``Group`` that are not editable.
+* Move invalid reference to ``Resource`` within UI tree rendering function under the relevant item rendering function.
+* Improve UI icons and messages related to ``User`` and ``Group`` edit not allowed when applicable to special members.
+
+Bug Fixes
+~~~~~~~~~~~~~~~~~~~~~
+* Fix ``matches`` operation of ``verify_param`` function to consider ignored case and verbose (extended) regexes as
+  defined in reference ``colander`` module to provide the intended validation.
+* Fix invalid line above UI tree hierarchy (resolves `#434 <https://github.com/Ouranosinc/Magpie/issues/434>`_).
+* Fix UI edit buttons for disallowed users (resolves `#457 <https://github.com/Ouranosinc/Magpie/issues/457>`_).
 
 `3.19.1 <https://github.com/Ouranosinc/Magpie/tree/3.19.1>`_ (2021-12-08)
 ------------------------------------------------------------------------------------
diff --git a/magpie/api/exception.py b/magpie/api/exception.py
index 90b8171c..d33e3884 100644
--- a/magpie/api/exception.py
+++ b/magpie/api/exception.py
@@ -228,7 +228,7 @@ def verify_param(  # noqa: E126  # pylint: disable=R0913,too-many-arguments
         fail_conditions.update({"is_type": isinstance(param, param_compare)})
         fail_verify = fail_verify or not fail_conditions["is_type"]
     if matches:
-        fail_conditions.update({"matches": bool(re.match(param_compare, param))})
+        fail_conditions.update({"matches": bool(re.match(param_compare, param, re.I | re.X))})
         fail_verify = fail_verify or not fail_conditions["matches"]
     if fail_verify:
         content = apply_param_content(content, param, param_compare, param_name, with_param, param_content,
diff --git a/magpie/ui/home/static/style.css b/magpie/ui/home/static/style.css
index be131984..39f060bd 100644
--- a/magpie/ui/home/static/style.css
+++ b/magpie/ui/home/static/style.css
@@ -845,12 +845,7 @@ table.simple-list input[type="submit"] {
     display: block;
 }
 
-.tree-key {
-    /* display: block;
-    float: left; */
-    display: inline-block;
-    position: absolute;
-
+.collapsible-tree-item {
     /* margin-top: 0.5em; */
     cursor: pointer;
 
@@ -859,6 +854,13 @@ table.simple-list input[type="submit"] {
     z-index: 10;
 }
 
+.tree-key {
+    /* display: block;
+    float: left; */
+    display: inline-block;
+    position: absolute;
+}
+
 .tree-item {
     /* display: block;
     float: right; */
@@ -867,6 +869,18 @@ table.simple-list input[type="submit"] {
     vertical-align: middle;
 }
 
+.tree-item-value {
+    display: inline-block;
+}
+
+.tree-item-label {
+    margin-right: 0.5em;
+}
+
+.tree-item-buttons {
+    float: right;
+}
+
 .tree-button.goto-service {
     margin: 0.25em 0 0 -4em;
 }
@@ -904,25 +918,14 @@ div.tree-button {
     /* ensure that sufficient space is provided to fit all possible permission-name side-by-side
        align with combobox and checkbox, must include both their respective width
     */
-    width: 10em;   /* [width = <desired-width> + permission-entry.width + permission-checkbox.width] */
     float: right;
     text-align: left;
+    margin-right: 0.5em;  /* spacing with disallowed text to ensure one title doesn't end flush where the next begins */
+    width: 9.5em;   /* [width = <desired-width> + permission-entry.width + permission-checkbox.width - margin-right] */
 
-    /* -webkit-transform: rotate(-10deg); */
-
-    /* Safari */
-
-    /* -moz-transform: rotate(-10deg); */
-
-    /* Firefox */
-
-    /* -ms-transform: rotate(-10deg); */
-
-    /* IE */
-
-    /* -o-transform: rotate(-10deg); */
-
-    /* Opera */
+    /* make text >width employ an ellipsis with scroll that auto apply/remove when using it for too long titles */
+    text-overflow: ellipsis;
+    overflow: auto;
 }
 
 .permission-title-effective {
diff --git a/magpie/ui/management/templates/edit_group.mako b/magpie/ui/management/templates/edit_group.mako
index 5aeb2f45..cef26a9d 100644
--- a/magpie/ui/management/templates/edit_group.mako
+++ b/magpie/ui/management/templates/edit_group.mako
@@ -80,6 +80,21 @@
                                 </div>
                             </form>
                         </td>
+                        <td>
+                            %if group_name in MAGPIE_FIXED_GROUP_EDITS:
+                                <div class="panel-form-lock">
+                                    <img src="${request.static_url('magpie.ui.home:static/lock.png')}"
+                                         alt="LOCKED" class="icon-locked"/>
+                                    <meta name="author"
+                                          content="https://www.flaticon.com/authors/those-icons">
+                                    <meta name="source"
+                                          content="https://www.flaticon.com/free-icon/lock_2089784">
+                                    <div class="alert-form-text alert-form-text-locked">
+                                        Edit not allowed for this special group.
+                                    </div>
+                                </div>
+                            %endif
+                        </td>
                     </tr>
                     <tr>
                         <td>
@@ -116,6 +131,8 @@
                                 </div>
                             </form>
                         </td>
+                        <td>
+                        </td>
                     </tr>
                     <tr>
                         <td>
@@ -141,6 +158,8 @@
                                 </div>
                             </form>
                         </td>
+                        <td>
+                        </td>
                     </tr>
                 </table>
             </div>
@@ -204,7 +223,7 @@ ${membership_alerts.edit_membership_alerts()}
     ])}
 
     <div class="current-tab-panel">
-        <div class="clear underline"></div>
+        <div class="clear"></div>
         %if error_message:
             <div class="alert alert-danger alert-visible">${error_message}</div>
         %endif
diff --git a/magpie/ui/management/templates/edit_service.mako b/magpie/ui/management/templates/edit_service.mako
index 23aeb987..37268521 100644
--- a/magpie/ui/management/templates/edit_service.mako
+++ b/magpie/ui/management/templates/edit_service.mako
@@ -251,34 +251,42 @@
     </div>
 </div>
 
-<%def name="render_item(_, value, level)">
-    <form id="resource_${value['id']}" action="${request.path}" method="post">
-        <input type="hidden" value="${value['id']}" name="resource_id">
-        <div class="tree-button">
-            <input type="submit" value="Delete" name="delete_child"
-            %if level > 0:
-                   class="button delete"
-            %else:
-                   class="button delete disabled" disabled
-            %endif
-            >
+<%def name="render_item(key, value, level)">
+    <div class="tree-item-value collapsible-tree-item">
+        <span class="tree-item-label label label-info">${value["resource_type"]}</span>
+        <div class="tree-key">
+            ${value.get('resource_display_name', key)}
         </div>
-        %if "id" in value.keys():
+    </div>
+    <div class="tree-item-buttons">
+        <form id="resource_${value['id']}" action="${request.path}" method="post">
+            <input type="hidden" value="${value['id']}" name="resource_id">
             <div class="tree-button">
-                <input type="submit" value="Add child" name="add_child"
-                %if int(value["id"]) in resources_id_type.keys():
-                    %if resources_id_type[int(value["id"])] in resources_no_child:
-                        class="button theme disabled" disabled
-                    %else:
-                        class="button theme"
-                    %endif
-                %elif not service_no_child:
-                        class="button theme"
+                <input type="submit" value="Delete" name="delete_child"
+                %if level > 0:
+                       class="button delete"
                 %else:
-                    class="button theme disabled" disabled
-                % endif
+                       class="button delete disabled" disabled
+                %endif
                 >
             </div>
-        %endif
-    </form>
+            %if "id" in value.keys():
+                <div class="tree-button">
+                    <input type="submit" value="Add child" name="add_child"
+                    %if int(value["id"]) in resources_id_type.keys():
+                        %if resources_id_type[int(value["id"])] in resources_no_child:
+                            class="button theme disabled" disabled
+                        %else:
+                            class="button theme"
+                        %endif
+                    %elif not service_no_child:
+                            class="button theme"
+                    %else:
+                        class="button theme disabled" disabled
+                    % endif
+                    >
+                </div>
+            %endif
+        </form>
+    </div>
 </%def>
diff --git a/magpie/ui/management/templates/edit_user.mako b/magpie/ui/management/templates/edit_user.mako
index 62e49a49..69ff596a 100644
--- a/magpie/ui/management/templates/edit_user.mako
+++ b/magpie/ui/management/templates/edit_user.mako
@@ -85,15 +85,25 @@
                             %endif
                         </td>
                         <td>
-                        %if invalid_user_name:
-                            <div class="panel-form-error">
-                                <img src="${request.static_url('magpie.ui.home:static/exclamation-circle.png')}"
-                                     alt="ERROR" class="icon-error" />
-                                <div class="alert-form-text alert-form-text-error">
-                                    ${reason_user_name}
+                            %if invalid_user_name and user_name not in MAGPIE_FIXED_USERS:
+                                <div class="panel-form-error">
+                                    <img src="${request.static_url('magpie.ui.home:static/exclamation-circle.png')}"
+                                         alt="ERROR" class="icon-error" />
+                                    <div class="alert-form-text alert-form-text-error">
+                                        ${reason_user_name}
+                                    </div>
                                 </div>
-                            </div>
-                        %endif
+                            %elif user_name in MAGPIE_FIXED_USERS:
+                                <div class="panel-form-lock">
+                                    <img src="${request.static_url('magpie.ui.home:static/lock.png')}"
+                                         alt="LOCKED" class="icon-locked"/>
+                                    <meta name="author" content="https://www.flaticon.com/authors/those-icons">
+                                    <meta name="source" content="https://www.flaticon.com/free-icon/lock_2089784">
+                                    <div class="alert-form-text alert-form-text-locked">
+                                        Edit not allowed for this special user.
+                                    </div>
+                                </div>
+                            %endif
                         </td>
                     </tr>
                     <tr>
@@ -101,30 +111,33 @@
                             <span class="panel-entry">Password: </span>
                         </td>
                         <td>
-                            <form id="edit_password" action="${request.path}" method="post">
+                            %if user_name not in MAGPIE_USER_PWD_DISABLED:
+                                <form id="edit_password" action="${request.path}" method="post">
+                            %endif
                                 <div class="panel-line-entry">
                                     %if edit_mode == "edit_password" and user_name not in MAGPIE_USER_PWD_DISABLED:
                                         <label>
-                                            <input type="password" placeholder="new password" name="new_user_password"
-                                                   id="input_password" value=""
+                                            <input type="password" placeholder="new password"
+                                                   name="new_user_password" id="input_password" value=""
                                                    onkeyup="adjustWidth('input_password')">
-                                            <input type="submit" value="Save" name="save_password" class="button theme">
-                                            <input type="submit" value="Cancel" name="no_edit" class="button cancel">
+                                            <input type="submit" value="Save" name="save_password"
+                                                   class="button theme">
+                                            <input type="submit" value="Cancel" name="no_edit"
+                                                   class="button cancel">
                                         </label>
                                     %else:
                                         <label>
                                             <span class="panel-value">***</span>
-                                            <input value="Edit" name="edit_password"
-                                                %if user_name in MAGPIE_USER_PWD_DISABLED:
-                                                   type="button" class="button theme disabled" disabled
-                                                %else:
-                                                   type="submit" class="button theme"
-                                                %endif
-                                            >
+                                            %if user_name not in MAGPIE_USER_PWD_DISABLED:
+                                                <input type="submit" value="Edit" name="edit_password"
+                                                       class="button theme">
+                                           %endif
                                         </label>
                                     %endif
                                 </div>
-                            </form>
+                            %if user_name not in MAGPIE_USER_PWD_DISABLED:
+                                </form>
+                            %endif
                         </td>
                         <td>
                         %if invalid_password:
@@ -135,19 +148,12 @@
                                     ${reason_password}
                                 </div>
                             </div>
-                        %elif user_name in MAGPIE_USER_PWD_LOCKED or user_name in MAGPIE_USER_PWD_DISABLED:
-                            <div class="panel-form-warning">
+                        %elif user_name in MAGPIE_FIXED_USERS:
+                            <div class="panel-form-lock">
                                 <img src="${request.static_url('magpie.ui.home:static/lock.png')}"
                                      alt="LOCKED" class="icon-locked"/>
                                 <meta name="author" content="https://www.flaticon.com/authors/those-icons">
                                 <meta name="source" content="https://www.flaticon.com/free-icon/lock_2089784">
-                                <div class="alert-form-text alert-form-text-locked">
-                                    %if user_name in MAGPIE_USER_PWD_LOCKED:
-                                        This special user password is locked and can only be edited from configuration.
-                                    %else:
-                                        This special user password is not editable.
-                                    %endif
-                                </div>
                             </div>
                         %endif
                         </td>
@@ -157,7 +163,9 @@
                             <span class="panel-entry">Email: </span>
                         </td>
                         <td>
-                            <form id="edit_email" action="${request.path}" method="post">
+                            %if user_name not in MAGPIE_FIXED_USERS:
+                                <form id="edit_email" action="${request.path}" method="post">
+                            %endif
                                 <div class="panel-line-entry">
                                     %if edit_mode == "edit_email":
                                         <label>
@@ -170,11 +178,15 @@
                                     %else:
                                         <label>
                                         <span class="panel-value">${email}</span>
-                                        <input type="submit" value="Edit" name="edit_email" class="button theme">
+                                        %if user_name not in MAGPIE_FIXED_USERS:
+                                            <input type="submit" value="Edit" name="edit_email" class="button theme">
+                                        %endif
                                         </label>
                                     %endif
                                 </div>
-                            </form>
+                            %if user_name not in MAGPIE_FIXED_USERS:
+                                </form>
+                            %endif
                         </td>
                         <td>
                         %if invalid_user_email:
@@ -185,6 +197,13 @@
                                     ${reason_user_email}
                                 </div>
                             </div>
+                        %elif user_name in MAGPIE_FIXED_USERS:
+                            <div class="panel-form-lock">
+                                <img src="${request.static_url('magpie.ui.home:static/lock.png')}"
+                                     alt="LOCKED" class="icon-locked"/>
+                                <meta name="author" content="https://www.flaticon.com/authors/those-icons">
+                                <meta name="source" content="https://www.flaticon.com/free-icon/lock_2089784">
+                            </div>
                         %endif
                         </td>
                     </tr>
@@ -204,6 +223,8 @@
                                 %endif
                             </div>
                         </td>
+                        <td>
+                        </td>
                     </tr>
                 </table>
             </div>
@@ -343,7 +364,7 @@ ${membership_alerts.edit_membership_alerts()}
     ])}
 
     <div class="current-tab-panel">
-        <div class="clear underline"></div>
+        <div class="clear"></div>
         %if error_message:
             <div class="alert alert-danger alert-visible">${error_message}</div>
         %endif
diff --git a/magpie/ui/management/templates/tree_scripts.js b/magpie/ui/management/templates/tree_scripts.js
index d822c7a2..ce6830bd 100644
--- a/magpie/ui/management/templates/tree_scripts.js
+++ b/magpie/ui/management/templates/tree_scripts.js
@@ -12,6 +12,6 @@ $(document).ready(function() {
         item.toggleClass("expanded");
     }
 
-    $(".tree-key").on("click", toggle)
+    $(".collapsible-tree-item").on("click", toggle)
     $(".collapsible-marker").on("click", toggle)
 })
diff --git a/magpie/ui/management/templates/tree_scripts.mako b/magpie/ui/management/templates/tree_scripts.mako
index 72dbd63f..4b1ee7cc 100644
--- a/magpie/ui/management/templates/tree_scripts.mako
+++ b/magpie/ui/management/templates/tree_scripts.mako
@@ -14,9 +14,6 @@
         <li class="no-child" id="${tree[key]['id']}">
         %endif
             <div class="tree-line">
-                <div class="tree-key">
-                    ${tree[key].get('resource_display_name', key)}
-                </div>
                 <div class="tree-item">
                     ${item_renderer(key, tree[key], level)}
                 </div>
@@ -48,7 +45,7 @@
         <div class="tree-header">
             <div class="tree-key">Resources</div>
             <div class="tree-item">
-                %for perm_name in permissions:
+                %for perm_name in permission_titles:
                     <div
                     %if inherit_groups_permissions:
                         class="permission-title permission-title-effective"
@@ -68,6 +65,12 @@
 
 <!-- renders a single resource line in the tree with applicable permission selectors for it -->
 <%def name="render_resource_permissions_item(key, value, level)">
+    <div class="tree-item-value collapsible-tree-item">
+        <span class="tree-item-label label label-info">${value["resource_type"]}</span>
+        <div class="tree-key">
+            ${value.get('resource_display_name', key)}
+        </div>
+    </div>
     %for perm_name in permissions:
         ${render_resource_permissions_entry(perm_name, value)}
     %endfor
diff --git a/magpie/ui/management/views.py b/magpie/ui/management/views.py
index 04655885..93d89dae 100644
--- a/magpie/ui/management/views.py
+++ b/magpie/ui/management/views.py
@@ -25,12 +25,12 @@ from magpie.cli.sync_resources import OUT_OF_SYNC
 from magpie.constants import get_constant
 # TODO: remove (REMOTE_RESOURCE_TREE_SERVICE, RESOURCE_TYPE_DICT), implement getters via API
 from magpie.models import REMOTE_RESOURCE_TREE_SERVICE, RESOURCE_TYPE_DICT, UserGroupStatus, UserStatuses
-from magpie.permissions import PermissionSet
+from magpie.permissions import Permission, PermissionSet
 from magpie.ui.utils import AdminRequests, BaseViews, check_response, handle_errors, request_api
 from magpie.utils import CONTENT_TYPE_JSON, get_json, get_logger
 
 if TYPE_CHECKING:
-    from typing import Dict, List, Optional, Tuple
+    from typing import Any, Dict, List, Optional, Tuple
 
     from sqlalchemy.orm.session import Session
 
@@ -300,6 +300,7 @@ class ManagementViews(AdminRequests, BaseViews):
         user_info["svc_types"] = svc_types
         user_info["resources"] = res_perms
         user_info["permissions"] = res_perm_names
+        user_info["permission_titles"] = [Permission(perm).title for perm in res_perm_names]
         return self.add_template_data(data=user_info)
 
     def view_pending_user(self):
@@ -410,11 +411,14 @@ class ManagementViews(AdminRequests, BaseViews):
             perm_names = [PermissionSet(perm_json).explicit_permission for perm_json in perms]
             children = self.resource_tree_parser(resource["children"], permission)
             children = OrderedDict(sorted(children.items()))
-            resources_tree[resource["resource_name"]] = dict(id=r_id,
-                                                             permissions=perms,
-                                                             permission_names=perm_names,
-                                                             resource_display_name=resource["resource_display_name"],
-                                                             children=children)
+            resources_tree[resource["resource_name"]] = dict(
+                id=r_id,
+                permissions=perms,
+                permission_names=perm_names,
+                resource_type=resource["resource_type"],
+                resource_display_name=resource["resource_display_name"],
+                children=children
+            )
         return resources_tree
 
     def perm_tree_parser(self, raw_perm_tree):
@@ -548,28 +552,19 @@ class ManagementViews(AdminRequests, BaseViews):
             perm_names = [PermissionSet(perm_json).explicit_permission for perm_json in perms]
             resources[service] = OrderedDict(
                 id=raw_resources["resource_id"],
+                resource_type="service",
                 permissions=perms,
                 permission_names=perm_names,
                 children=self.resource_tree_parser(raw_resources["resources"], permission))
         return resources_permission_names, resources
 
-    def update_user_or_group_resources_permissions_dict(self, res_perms, updated_perms):
-        for res in res_perms.values():
-            perms = updated_perms.get(str(res["id"]), [])
-            if perms:
-                res["permissions"] = perms
-                return True
-            if self.update_user_or_group_resources_permissions_dict(res["children"], updated_perms):
-                return True
-        return False
-
     @view_config(route_name="edit_group", renderer="templates/edit_group.mako")
     def edit_group(self):
         group_name = self.request.matchdict["group_name"]
         cur_svc_type = self.request.matchdict["cur_svc_type"]
         group_info = {"edit_mode": "no_edit", "group_name": group_name, "cur_svc_type": cur_svc_type}
         error_message = ""
-        edit_grp_usrs_info = {}
+        edit_grp_users_info = {}
 
         # TODO:
         #   Until the api is modified to make it possible to request from the RemoteResource table,
@@ -635,7 +630,7 @@ class ManagementViews(AdminRequests, BaseViews):
 
             # edits to group members checkboxes
             if is_edit_group_members:
-                edit_grp_usrs_info = self.edit_group_users(group_name)
+                edit_grp_users_info = self.edit_group_users(group_name)
 
         # display resources permissions per service type tab
         try:
@@ -667,11 +662,13 @@ class ManagementViews(AdminRequests, BaseViews):
         group_info["cur_svc_type"] = cur_svc_type
         group_info["resources"] = res_perms
         group_info["permissions"] = res_perm_names
+        group_info["permission_titles"] = [Permission(perm).title for perm in res_perm_names]
 
-        if edit_grp_usrs_info:
-            new_usrs_from_pending = edit_grp_usrs_info["edit_new_membership_success"] & set(group_info["pending_users"])
-            group_info["edit_membership_pending_success"] = new_usrs_from_pending
-            group_info["edit_new_membership_error"] = edit_grp_usrs_info["edit_new_membership_error"]
+        if edit_grp_users_info:
+            group_info["edit_membership_pending_success"] = (
+                edit_grp_users_info["edit_new_membership_success"] & set(group_info["pending_users"])
+            )
+            group_info["edit_new_membership_error"] = edit_grp_users_info["edit_new_membership_error"]
         return self.add_template_data(data=group_info)
 
     @staticmethod
@@ -800,6 +797,7 @@ class ManagementViews(AdminRequests, BaseViews):
         resources[service_name] = dict(
             id=raw_resources["resource_id"],
             permissions=[],
+            resource_type="service",
             children=self.resource_tree_parser(raw_resources["resources"], {}))
         resources_id_type = self.get_resource_types()
         return resources, resources_id_type
@@ -888,7 +886,7 @@ class ManagementViews(AdminRequests, BaseViews):
             "service_push": service_push,
             "service_push_show": service_push_show,
             "cur_svc_type": cur_svc_type,
-        }
+        }  # type: Dict[str, Any]
 
         svc_config = service_data["configuration"]
         if not svc_config:
diff --git a/magpie/ui/user/templates/edit_current_user.mako b/magpie/ui/user/templates/edit_current_user.mako
index 5349952d..eb24c82a 100644
--- a/magpie/ui/user/templates/edit_current_user.mako
+++ b/magpie/ui/user/templates/edit_current_user.mako
@@ -92,7 +92,17 @@
                                 </label>
                             </div>
                         </td>
-                        <td></td>
+                        <td>
+                            %if user_name in MAGPIE_USER_PWD_LOCKED:
+                                <div class="panel-form-warning">
+                                    <img src="${request.static_url('magpie.ui.home:static/exclamation-triangle.png')}"
+                                         alt="WARNING" class="icon-warning" />
+                                    <div class="alert-form-text alert-form-text-warning">
+                                        This special user is locked and can only be edited from configuration.
+                                    </div>
+                                </div>
+                            %endif
+                        </td>
                     </tr>
                     <tr>
                         <td>
@@ -137,9 +147,7 @@
                                 <img src="${request.static_url('magpie.ui.home:static/exclamation-triangle.png')}"
                                      alt="WARNING" class="icon-warning" />
                                 <div class="alert-form-text alert-form-text-warning">
-                                    %if user_name in MAGPIE_USER_PWD_LOCKED:
-                                        This special user password is locked and can only be edited from configuration.
-                                    %else:
+                                    %if user_name not in MAGPIE_USER_PWD_LOCKED:
                                         This special user password is not editable.
                                     %endif
                                 </div>
@@ -166,7 +174,7 @@
                                     %else:
                                         <label>
                                             <span class="panel-value">${email}</span>
-                                            %if user_edit_email:
+                                            %if user_edit_email and user_name not in MAGPIE_USER_PWD_LOCKED:
                                             <input type="submit" value="Edit" name="edit_email" class="button theme">
                                             %else:
                                             <input type="button" value="Edit" name="edit_email"
@@ -197,6 +205,11 @@
                                     Email edition not allowed. Please contact the platform administrator.
                                 </div>
                             </div>
+                        %elif user_name in MAGPIE_USER_PWD_LOCKED:
+                            <div class="panel-form-warning">
+                                <img src="${request.static_url('magpie.ui.home:static/exclamation-triangle.png')}"
+                                             alt="WARNING" class="icon-warning" />
+                            </div>
                         %endif
                         </td>
                     </tr>
diff --git a/magpie/ui/utils.py b/magpie/ui/utils.py
index 4eddab58..5180d743 100644
--- a/magpie/ui/utils.py
+++ b/magpie/ui/utils.py
@@ -180,7 +180,7 @@ class BaseViews(object):
     MAGPIE_FIXED_GROUP_MEMBERSHIPS = []
     MAGPIE_FIXED_GROUP_EDITS = []
     MAGPIE_FIXED_USERS = []
-    MAGPIE_USER_PWD_LOCKED = []
+    MAGPIE_USER_PWD_LOCKED = []  # used when user self-edit itself but is not supported since disabled
     MAGPIE_USER_PWD_DISABLED = []
     MAGPIE_ANONYMOUS_GROUP = None
 
diff --git a/magpie/utils.py b/magpie/utils.py
index 78557b53..9c5da92c 100644
--- a/magpie/utils.py
+++ b/magpie/utils.py
@@ -872,6 +872,24 @@ class ExtendedEnum(Enum):
                 return m_val
         return default
 
+    @classmethod
+    def titles(cls):
+        # type: () -> List[Str]
+        """
+        Returns the title representation of all enum elements.
+        """
+        return list(member.title for member in cls.__members__.values())
+
+    @property
+    def title(self):
+        # type: () -> Str
+        """
+        Returns the title representation of the enum element.
+
+        Title use the original enum element name with capitalization considering underscores for separate words.
+        """
+        return self.name.title().replace("_", "")
+
 
 # note: must not define any enum value here to allow inheritance by subclasses
 class FlexibleNameEnum(ExtendedEnum):
diff --git a/requirements-sys.txt b/requirements-sys.txt
index 923f0260..495c2654 100644
--- a/requirements-sys.txt
+++ b/requirements-sys.txt
@@ -1,2 +1,2 @@
 pip
-setuptools
+setuptools<60


Test patch:
diff --git a/tests/interfaces.py b/tests/interfaces.py
index 4f15e247..bd197032 100644
--- a/tests/interfaces.py
+++ b/tests/interfaces.py
@@ -6388,7 +6388,7 @@ class Interface_MagpieUI_AdminAuth(AdminTestCase, BaseTestCase):
         resp = utils.TestSetup.check_FormSubmit(self, form_match="toggle_visible_perms", form_data=data, path=path)
         # find displayed resources/permissions in hierarchy
         hierarchy = {svc_id: {res1_id: {res2_id: {}, res3_id: {}, res4_id: {}, res5_id: {}}}}
-        res_perms = utils.find_html_resource_tree_permissions(resp, perm, hierarchy)
+        res_perms = utils.find_html_resource_tree_permissions(resp, perm, hierarchy, version=self.version)
 
         utils.check_val_equal(res_perms[svc_id], rAR)
         utils.check_val_equal(res_perms[res1_id], rAR)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 5519e83a..783e4c03 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -25,8 +25,8 @@ from tests import runner, utils
 
 
 class DummyEnum(ExtendedEnum):
-    VALUE1 = "value-1"
-    VALUE2 = "value-2"
+    TEST_VALUE_1 = "value-1"
+    TEST_VALUE_2 = "value-2"
 
 
 @runner.MAGPIE_TEST_LOCAL
@@ -136,7 +136,7 @@ class TestUtils(unittest.TestCase):
         utils.check_raises(lambda: ax.verify_param(1, is_none=True), HTTPBadRequest)
         utils.check_raises(lambda: ax.verify_param("", not_empty=True), HTTPBadRequest)
         utils.check_raises(lambda: ax.verify_param("abc", is_empty=True), HTTPBadRequest)
-        utils.check_raises(lambda: ax.verify_param("abc", matches=True, param_compare=r"[A-Z]+"), HTTPBadRequest)
+        utils.check_raises(lambda: ax.verify_param("abc", matches=True, param_compare=r"[0-9]+"), HTTPBadRequest)
 
         # with requested error
         utils.check_raises(lambda:
@@ -157,7 +157,7 @@ class TestUtils(unittest.TestCase):
         utils.check_raises(lambda: ax.verify_param("", not_empty=True, http_error=HTTPForbidden), HTTPForbidden)
         utils.check_raises(lambda: ax.verify_param("abc", is_empty=True, http_error=HTTPForbidden), HTTPForbidden)
         utils.check_raises(lambda:
-                           ax.verify_param("abc", matches=True, param_compare=r"[A-Z]+", http_error=HTTPForbidden),
+                           ax.verify_param("abc", matches=True, param_compare=r"[0-9]+", http_error=HTTPForbidden),
                            HTTPForbidden)
 
     def test_verify_param_proper_verifications_passed(self):
@@ -243,16 +243,22 @@ class TestUtils(unittest.TestCase):
         utils.check_all_equal(DummyEnum.values(), ["value-1", "value-2"], any_order=True)
 
     def test_enum_get_by_value(self):
-        utils.check_val_equal(DummyEnum.get("value-1"), DummyEnum.VALUE1)
-        utils.check_val_equal(DummyEnum.get("VALUE1"), DummyEnum.VALUE1)
+        utils.check_val_equal(DummyEnum.get("value-1"), DummyEnum.TEST_VALUE_1)
+        utils.check_val_equal(DummyEnum.get("TEST_VALUE_1"), DummyEnum.TEST_VALUE_1)
         utils.check_val_equal(DummyEnum.get("random"), None)
         utils.check_val_equal(DummyEnum.get("random", "something"), "something")
 
     def test_enum_other(self):
         class OtherEnum(ExtendedEnum):
-            VALUE1 = DummyEnum.VALUE1.value  # copy internal string representation
+            TEST_VALUE_1 = DummyEnum.TEST_VALUE_1.value  # copy internal string representation
 
-        utils.check_val_not_equal(DummyEnum.VALUE1, OtherEnum.VALUE1, msg="concrete enum elements should be different")
+        utils.check_val_not_equal(DummyEnum.TEST_VALUE_1, OtherEnum.TEST_VALUE_1,
+                                  msg="concrete enum elements should be different")
+
+    def test_enum_titles(self):
+        utils.check_val_equal(DummyEnum.TEST_VALUE_1.title, "TestValue1")
+        utils.check_val_equal(DummyEnum.TEST_VALUE_2.title, "TestValue2")
+        utils.check_val_equal(DummyEnum.titles(), ["TestValue1", "TestValue2"])
 
     def test_evaluate_call_callable_incorrect_usage(self):
         """
diff --git a/tests/utils.py b/tests/utils.py
index b5807cdc..8a3c7829 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1587,6 +1587,7 @@ def find_html_form(forms, form_match):
 def find_html_resource_tree_permissions(response_or_body,   # type: Union[TestResponse, BeautifulSoup]
                                         permission,         # type: AnyPermissionType
                                         resource_tree,      # type: JSON
+                                        version="latest",   # type: Str
                                         ):                  # type: (...) -> Dict[int, Optional[PermissionSet]]
     """
     Retrieves all displayed permissions within combo-boxes of a resource-tree Magpie UI HTML page.
@@ -1609,10 +1610,11 @@ def find_html_resource_tree_permissions(response_or_body,   # type: Union[TestRe
     :param response_or_body: Magpie UI HTML response to search for contents, or directly an HTML content object.
     :param permission: permission (column name) to obtain from the page. Can be any permission type to extract the name.
     :param resource_tree: dictionary of expected tree hierarchy of nested resource IDs to extract.
+    :param version: Magpie application version to be considered for lookup of contents.
     :return: flat dictionary of resource IDs to displayed permission (or empty string if no permission displayed) .
     """
     # find resources/permissions hierarchy container
-    perm_name = PermissionSet(permission).name.value
+    perm = PermissionSet(permission).name
     perm_form = find_html_body_contents(response_or_body, [
         {"class": ["content"]}, {"class": ["tabs-panel"]},
         {"class": ["current-tab-panel"]}, {"id": "resources_permissions"}
@@ -1622,7 +1624,7 @@ def find_html_resource_tree_permissions(response_or_body,   # type: Union[TestRe
         {"class": ["tree-header"]}, {"class": ["tree-item"]}, {"class": ["permission-title"]}
     ])
     perm_titles = [perm.text for perm in perm_header]
-    perm_index = perm_titles.index(perm_name)
+    perm_index = perm_titles.index(perm.title if TestVersion(version) >= TestVersion("3.20") else perm.value)
 
     found_res_perms = {}