prompt: 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()
  
  ```
  
golden_diff: 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]
  
verification_info: {"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"}]}