We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
TypeError thrown when during groups lookup
The offending code is [here](https://github.com/plone/Products.CMFPlone/blob/308aa4d03ee6c0ce9d8119ce4c37955153f0bc6f/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py#L66). The traceback looks like this:
```
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 176, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 385, in publish_module
  Module ZPublisher.WSGIPublisher, line 280, in publish
  Module ZPublisher.mapply, line 85, in mapply
  Module ZPublisher.WSGIPublisher, line 63, in call_object
  Module Products.CMFPlone.controlpanel.browser.usergroups_usermembership, line 57, in __call__
  Module Products.CMFPlone.controlpanel.browser.usergroups_usermembership, line 54, in update
  Module Products.CMFPlone.controlpanel.browser.usergroups_usermembership, line 63, in getGroups
TypeError: '<' not supported between instances of 'bool' and 'str'
```

The issue is that when there's a `None` value in the `groupResults` (which is anticipated in the sort code) the lambda returns `False` which fails to compare against the group title/name strings under Python 3. The list comprehension that defines `groupResults` should probably just filter out `None` values to avoid this issue. I'm not entirely sure what circumstances result in a `None` group value, but I am seeing it occur in a real world use case.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py`
Content:
```
1 from plone.base import PloneMessageFactory as _
2 from Products.CMFCore.utils import getToolByName
3 from Products.CMFPlone.controlpanel.browser.usergroups import (
4     UsersGroupsControlPanelView,
5 )
6 from Products.CMFPlone.utils import normalizeString
7 from zExceptions import Forbidden
8 
9 
10 class UserMembershipControlPanel(UsersGroupsControlPanelView):
11     def update(self):
12         self.userid = getattr(self.request, "userid")
13         self.gtool = getToolByName(self, "portal_groups")
14         self.mtool = getToolByName(self, "portal_membership")
15         self.member = self.mtool.getMemberById(self.userid)
16 
17         form = self.request.form
18 
19         self.searchResults = []
20         self.searchString = ""
21         self.newSearch = False
22 
23         if form.get("form.submitted", False):
24             delete = form.get("delete", [])
25             if delete:
26                 for groupname in delete:
27                     self.gtool.removePrincipalFromGroup(
28                         self.userid, groupname, self.request
29                     )
30                 self.context.plone_utils.addPortalMessage(_("Changes made."))
31 
32             add = form.get("add", [])
33             if add:
34                 for groupname in add:
35                     group = self.gtool.getGroupById(groupname)
36                     if "Manager" in group.getRoles() and not self.is_zope_manager:
37                         raise Forbidden
38 
39                     self.gtool.addPrincipalToGroup(self.userid, groupname, self.request)
40                 self.context.plone_utils.addPortalMessage(_("Changes made."))
41 
42         search = form.get("form.button.Search", None) is not None
43         findAll = (
44             form.get("form.button.FindAll", None) is not None and not self.many_groups
45         )
46         self.searchString = not findAll and form.get("searchstring", "") or ""
47 
48         if findAll or not self.many_groups or self.searchString != "":
49             self.searchResults = self.getPotentialGroups(self.searchString)
50 
51         if search or findAll:
52             self.newSearch = True
53 
54         self.groups = self.getGroups()
55 
56     def __call__(self):
57         self.update()
58         return self.index()
59 
60     def getGroups(self):
61         groupResults = [
62             self.gtool.getGroupById(m)
63             for m in self.gtool.getGroupsForPrincipal(self.member)
64         ]
65         groupResults.sort(
66             key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())
67         )
68         return [i for i in groupResults if i]
69 
70     def getPotentialGroups(self, searchString):
71         ignoredGroups = [x.id for x in self.getGroups() if x is not None]
72         return self.membershipSearch(
73             searchString, searchUsers=False, ignore=ignoredGroups
74         )
75
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py
--- a/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py
+++ b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py
@@ -63,7 +63,7 @@
             for m in self.gtool.getGroupsForPrincipal(self.member)
         ]
         groupResults.sort(
-            key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())
+            key=lambda x: normalizeString(x.getGroupTitleOrName()) if x else ''
         )
         return [i for i in groupResults if i]

{"golden_diff": "diff --git a/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py\n--- a/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py\n+++ b/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py\n@@ -63,7 +63,7 @@\n             for m in self.gtool.getGroupsForPrincipal(self.member)\n         ]\n         groupResults.sort(\n-            key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())\n+            key=lambda x: normalizeString(x.getGroupTitleOrName()) if x else ''\n         )\n         return [i for i in groupResults if i]\n", "issue": "TypeError thrown when during groups lookup\nThe offending code is [here](https://github.com/plone/Products.CMFPlone/blob/308aa4d03ee6c0ce9d8119ce4c37955153f0bc6f/Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py#L66). The traceback looks like this:\r\n```\r\nTraceback (innermost last):\r\n  Module ZPublisher.WSGIPublisher, line 176, in transaction_pubevents\r\n  Module ZPublisher.WSGIPublisher, line 385, in publish_module\r\n  Module ZPublisher.WSGIPublisher, line 280, in publish\r\n  Module ZPublisher.mapply, line 85, in mapply\r\n  Module ZPublisher.WSGIPublisher, line 63, in call_object\r\n  Module Products.CMFPlone.controlpanel.browser.usergroups_usermembership, line 57, in __call__\r\n  Module Products.CMFPlone.controlpanel.browser.usergroups_usermembership, line 54, in update\r\n  Module Products.CMFPlone.controlpanel.browser.usergroups_usermembership, line 63, in getGroups\r\nTypeError: '<' not supported between instances of 'bool' and 'str'\r\n```\r\n\r\nThe issue is that when there's a `None` value in the `groupResults` (which is anticipated in the sort code) the lambda returns `False` which fails to compare against the group title/name strings under Python 3. The list comprehension that defines `groupResults` should probably just filter out `None` values to avoid this issue. I'm not entirely sure what circumstances result in a `None` group value, but I am seeing it occur in a real world use case.\n", "before_files": [{"content": "from plone.base import PloneMessageFactory as _\nfrom Products.CMFCore.utils import getToolByName\nfrom Products.CMFPlone.controlpanel.browser.usergroups import (\n    UsersGroupsControlPanelView,\n)\nfrom Products.CMFPlone.utils import normalizeString\nfrom zExceptions import Forbidden\n\n\nclass UserMembershipControlPanel(UsersGroupsControlPanelView):\n    def update(self):\n        self.userid = getattr(self.request, \"userid\")\n        self.gtool = getToolByName(self, \"portal_groups\")\n        self.mtool = getToolByName(self, \"portal_membership\")\n        self.member = self.mtool.getMemberById(self.userid)\n\n        form = self.request.form\n\n        self.searchResults = []\n        self.searchString = \"\"\n        self.newSearch = False\n\n        if form.get(\"form.submitted\", False):\n            delete = form.get(\"delete\", [])\n            if delete:\n                for groupname in delete:\n                    self.gtool.removePrincipalFromGroup(\n                        self.userid, groupname, self.request\n                    )\n                self.context.plone_utils.addPortalMessage(_(\"Changes made.\"))\n\n            add = form.get(\"add\", [])\n            if add:\n                for groupname in add:\n                    group = self.gtool.getGroupById(groupname)\n                    if \"Manager\" in group.getRoles() and not self.is_zope_manager:\n                        raise Forbidden\n\n                    self.gtool.addPrincipalToGroup(self.userid, groupname, self.request)\n                self.context.plone_utils.addPortalMessage(_(\"Changes made.\"))\n\n        search = form.get(\"form.button.Search\", None) is not None\n        findAll = (\n            form.get(\"form.button.FindAll\", None) is not None and not self.many_groups\n        )\n        self.searchString = not findAll and form.get(\"searchstring\", \"\") or \"\"\n\n        if findAll or not self.many_groups or self.searchString != \"\":\n            self.searchResults = self.getPotentialGroups(self.searchString)\n\n        if search or findAll:\n            self.newSearch = True\n\n        self.groups = self.getGroups()\n\n    def __call__(self):\n        self.update()\n        return self.index()\n\n    def getGroups(self):\n        groupResults = [\n            self.gtool.getGroupById(m)\n            for m in self.gtool.getGroupsForPrincipal(self.member)\n        ]\n        groupResults.sort(\n            key=lambda x: x is not None and normalizeString(x.getGroupTitleOrName())\n        )\n        return [i for i in groupResults if i]\n\n    def getPotentialGroups(self, searchString):\n        ignoredGroups = [x.id for x in self.getGroups() if x is not None]\n        return self.membershipSearch(\n            searchString, searchUsers=False, ignore=ignoredGroups\n        )\n", "path": "Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py"}], "after_files": [{"content": "from plone.base import PloneMessageFactory as _\nfrom Products.CMFCore.utils import getToolByName\nfrom Products.CMFPlone.controlpanel.browser.usergroups import (\n    UsersGroupsControlPanelView,\n)\nfrom Products.CMFPlone.utils import normalizeString\nfrom zExceptions import Forbidden\n\n\nclass UserMembershipControlPanel(UsersGroupsControlPanelView):\n    def update(self):\n        self.userid = getattr(self.request, \"userid\")\n        self.gtool = getToolByName(self, \"portal_groups\")\n        self.mtool = getToolByName(self, \"portal_membership\")\n        self.member = self.mtool.getMemberById(self.userid)\n\n        form = self.request.form\n\n        self.searchResults = []\n        self.searchString = \"\"\n        self.newSearch = False\n\n        if form.get(\"form.submitted\", False):\n            delete = form.get(\"delete\", [])\n            if delete:\n                for groupname in delete:\n                    self.gtool.removePrincipalFromGroup(\n                        self.userid, groupname, self.request\n                    )\n                self.context.plone_utils.addPortalMessage(_(\"Changes made.\"))\n\n            add = form.get(\"add\", [])\n            if add:\n                for groupname in add:\n                    group = self.gtool.getGroupById(groupname)\n                    if \"Manager\" in group.getRoles() and not self.is_zope_manager:\n                        raise Forbidden\n\n                    self.gtool.addPrincipalToGroup(self.userid, groupname, self.request)\n                self.context.plone_utils.addPortalMessage(_(\"Changes made.\"))\n\n        search = form.get(\"form.button.Search\", None) is not None\n        findAll = (\n            form.get(\"form.button.FindAll\", None) is not None and not self.many_groups\n        )\n        self.searchString = not findAll and form.get(\"searchstring\", \"\") or \"\"\n\n        if findAll or not self.many_groups or self.searchString != \"\":\n            self.searchResults = self.getPotentialGroups(self.searchString)\n\n        if search or findAll:\n            self.newSearch = True\n\n        self.groups = self.getGroups()\n\n    def __call__(self):\n        self.update()\n        return self.index()\n\n    def getGroups(self):\n        groupResults = [\n            self.gtool.getGroupById(m)\n            for m in self.gtool.getGroupsForPrincipal(self.member)\n        ]\n        groupResults.sort(\n            key=lambda x: normalizeString(x.getGroupTitleOrName()) if x else ''\n        )\n        return [i for i in groupResults if i]\n\n    def getPotentialGroups(self, searchString):\n        ignoredGroups = [x.id for x in self.getGroups() if x is not None]\n        return self.membershipSearch(\n            searchString, searchUsers=False, ignore=ignoredGroups\n        )\n", "path": "Products/CMFPlone/controlpanel/browser/usergroups_usermembership.py"}]}