We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Improve the user search and selection form
In #1684 the user selection for groups was consolidated, so now it would be good to improve it. The form uses a ModelSelect2 widget (https://github.com/comic/grand-challenge.org/blob/master/app/grandchallenge/groups/forms.py#L19) that calls an autocomplete view that only allows filtering by username (https://github.com/comic/grand-challenge.org/blob/master/app/grandchallenge/groups/views.py#L67). 

Top priority is to extend the filter:
- Switch to filtering with `icontains` rather than `istartswith`
- Allow filtering on the users username, email address, full name, OR verification email address. To do this use Django `Q` objects (https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q-objects).

Next, is to improve the look of the widget, at the moment you just get a string of the username:
![image](https://user-images.githubusercontent.com/12661555/105184656-e1834880-5b2f-11eb-9ac7-36c9e2fcff22.png)

On github it's much nicer
![image](https://user-images.githubusercontent.com/12661555/105184861-214a3000-5b30-11eb-9fe5-24aa1c4613a4.png)

Extend this to
- Include the users full name in the string
- Show whether the user is verified or not in the string, and if so, what is the domain of their verification address (see https://github.com/comic/grand-challenge.org/blob/master/app/grandchallenge/profiles/templatetags/profiles.py#L16)
- Stretch goal: Show the users mugshot


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `app/grandchallenge/groups/forms.py`
Content:
```
1 from dal import autocomplete
2 from django.conf import settings
3 from django.contrib.auth import get_user_model
4 from django.core.exceptions import ValidationError
5 from django.forms import ChoiceField, Form, HiddenInput, ModelChoiceField
6 from guardian.utils import get_anonymous_user
7 
8 from grandchallenge.core.forms import SaveFormInitMixin
9 
10 
11 class UserGroupForm(SaveFormInitMixin, Form):
12     ADD = "ADD"
13     REMOVE = "REMOVE"
14     CHOICES = ((ADD, "Add"), (REMOVE, "Remove"))
15     user = ModelChoiceField(
16         queryset=get_user_model().objects.all().order_by("username"),
17         help_text="Select a user that will be added to the group",
18         required=True,
19         widget=autocomplete.ModelSelect2(
20             url="users-autocomplete",
21             attrs={
22                 "data-placeholder": "Search for a user ...",
23                 "data-minimum-input-length": 3,
24                 "data-theme": settings.CRISPY_TEMPLATE_PACK,
25             },
26         ),
27     )
28     action = ChoiceField(
29         choices=CHOICES, required=True, widget=HiddenInput(), initial=ADD
30     )
31 
32     def clean_user(self):
33         user = self.cleaned_data["user"]
34         if user == get_anonymous_user():
35             raise ValidationError("You cannot add this user!")
36         return user
37 
38     def add_or_remove_user(self, *, obj):
39         if self.cleaned_data["action"] == self.ADD:
40             getattr(obj, f"add_{self.role}")(self.cleaned_data["user"])
41         elif self.cleaned_data["action"] == self.REMOVE:
42             getattr(obj, f"remove_{self.role}")(self.cleaned_data["user"])
43 
44 
45 class EditorsForm(UserGroupForm):
46     role = "editor"
47 
48 
49 class MembersForm(UserGroupForm):
50     role = "member"
51 
52 
53 class UsersForm(UserGroupForm):
54     role = "user"
55
```

Path: `app/grandchallenge/groups/views.py`
Content:
```
1 from dal import autocomplete
2 from django.conf import settings
3 from django.contrib.auth import get_user_model
4 from django.contrib.auth.mixins import UserPassesTestMixin
5 from django.contrib.messages.views import SuccessMessageMixin
6 from django.views.generic import FormView
7 from guardian.mixins import (
8     LoginRequiredMixin,
9     PermissionRequiredMixin as ObjectPermissionRequiredMixin,
10 )
11 from guardian.shortcuts import get_objects_for_user
12 
13 
14 class UserGroupUpdateMixin(
15     LoginRequiredMixin,
16     ObjectPermissionRequiredMixin,
17     SuccessMessageMixin,
18     FormView,
19 ):
20     raise_exception = True
21 
22     def get_permission_object(self):
23         return self.obj
24 
25     @property
26     def obj(self):
27         raise NotImplementedError
28 
29     def get_context_data(self, **kwargs):
30         context = super().get_context_data(**kwargs)
31         context.update({"object": self.obj, "role": self.get_form().role})
32         return context
33 
34     def get_success_url(self):
35         return self.obj.get_absolute_url()
36 
37     def form_valid(self, form):
38         form.add_or_remove_user(obj=self.obj)
39         return super().form_valid(form)
40 
41 
42 class UserAutocomplete(
43     LoginRequiredMixin, UserPassesTestMixin, autocomplete.Select2QuerySetView
44 ):
45     def test_func(self):
46         allowed_perms = [
47             "algorithms.change_algorithm",
48             "organizations.change_organization",
49             "archives.change_archive",
50             "reader_studies.change_readerstudy",
51             "workstations.change_workstation",
52             "algorithms.change_job",
53         ]
54         return any(
55             get_objects_for_user(user=self.request.user, perms=perm,).exists()
56             for perm in allowed_perms
57         )
58 
59     def get_queryset(self):
60         qs = (
61             get_user_model()
62             .objects.order_by("username")
63             .exclude(username=settings.ANONYMOUS_USER_NAME)
64         )
65 
66         if self.q:
67             qs = qs.filter(username__istartswith=self.q)
68 
69         return qs
70
```
--- 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/app/grandchallenge/groups/forms.py b/app/grandchallenge/groups/forms.py
--- a/app/grandchallenge/groups/forms.py
+++ b/app/grandchallenge/groups/forms.py
@@ -22,6 +22,7 @@
                 "data-placeholder": "Search for a user ...",
                 "data-minimum-input-length": 3,
                 "data-theme": settings.CRISPY_TEMPLATE_PACK,
+                "data-html": True,
             },
         ),
     )
diff --git a/app/grandchallenge/groups/views.py b/app/grandchallenge/groups/views.py
--- a/app/grandchallenge/groups/views.py
+++ b/app/grandchallenge/groups/views.py
@@ -3,6 +3,9 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.contrib.messages.views import SuccessMessageMixin
+from django.db.models import CharField, Q, Value
+from django.db.models.functions import Concat
+from django.utils.html import format_html
 from django.views.generic import FormView
 from guardian.mixins import (
     LoginRequiredMixin,
@@ -10,6 +13,8 @@
 )
 from guardian.shortcuts import get_objects_for_user
 
+from grandchallenge.verifications.models import Verification
+
 
 class UserGroupUpdateMixin(
     LoginRequiredMixin,
@@ -51,6 +56,7 @@
             "workstations.change_workstation",
             "algorithms.change_job",
         ]
+        # TODO reduce number of queries
         return any(
             get_objects_for_user(user=self.request.user, perms=perm,).exists()
             for perm in allowed_perms
@@ -61,9 +67,50 @@
             get_user_model()
             .objects.order_by("username")
             .exclude(username=settings.ANONYMOUS_USER_NAME)
+            .annotate(
+                full_name=Concat(
+                    "first_name",
+                    Value(" "),
+                    "last_name",
+                    output_field=CharField(),
+                )
+            )
+            .select_related("verification", "user_profile")
         )
 
         if self.q:
-            qs = qs.filter(username__istartswith=self.q)
+            qs = qs.filter(
+                Q(username__icontains=self.q)
+                | Q(email__icontains=self.q)
+                | Q(full_name__icontains=self.q)
+                | Q(verification__email__icontains=self.q)
+            )
 
         return qs
+
+    def get_result_label(self, result):
+
+        try:
+            is_verified = result.verification.is_verified
+        except Verification.DoesNotExist:
+            is_verified = False
+
+        if is_verified:
+            return format_html(
+                '<img src="{}" width ="20" height ="20" style="vertical-align:top"> '
+                "&nbsp; <b>{}</b> &nbsp; {} &nbsp;"
+                '<i class="fas fa-user-check text-success" '
+                'title="Verified email address at {}">',
+                result.user_profile.get_mugshot_url(),
+                result.get_username(),
+                result.get_full_name().title(),
+                result.verification.email.split("@")[1],
+            )
+        else:
+            return format_html(
+                '<img src="{}" width ="20" height ="20" style="vertical-align:top"> '
+                "&nbsp; <b>{}</b> &nbsp; {}",
+                result.user_profile.get_mugshot_url(),
+                result.get_username(),
+                result.get_full_name().title(),
+            )

{"golden_diff": "diff --git a/app/grandchallenge/groups/forms.py b/app/grandchallenge/groups/forms.py\n--- a/app/grandchallenge/groups/forms.py\n+++ b/app/grandchallenge/groups/forms.py\n@@ -22,6 +22,7 @@\n                 \"data-placeholder\": \"Search for a user ...\",\n                 \"data-minimum-input-length\": 3,\n                 \"data-theme\": settings.CRISPY_TEMPLATE_PACK,\n+                \"data-html\": True,\n             },\n         ),\n     )\ndiff --git a/app/grandchallenge/groups/views.py b/app/grandchallenge/groups/views.py\n--- a/app/grandchallenge/groups/views.py\n+++ b/app/grandchallenge/groups/views.py\n@@ -3,6 +3,9 @@\n from django.contrib.auth import get_user_model\n from django.contrib.auth.mixins import UserPassesTestMixin\n from django.contrib.messages.views import SuccessMessageMixin\n+from django.db.models import CharField, Q, Value\n+from django.db.models.functions import Concat\n+from django.utils.html import format_html\n from django.views.generic import FormView\n from guardian.mixins import (\n     LoginRequiredMixin,\n@@ -10,6 +13,8 @@\n )\n from guardian.shortcuts import get_objects_for_user\n \n+from grandchallenge.verifications.models import Verification\n+\n \n class UserGroupUpdateMixin(\n     LoginRequiredMixin,\n@@ -51,6 +56,7 @@\n             \"workstations.change_workstation\",\n             \"algorithms.change_job\",\n         ]\n+        # TODO reduce number of queries\n         return any(\n             get_objects_for_user(user=self.request.user, perms=perm,).exists()\n             for perm in allowed_perms\n@@ -61,9 +67,50 @@\n             get_user_model()\n             .objects.order_by(\"username\")\n             .exclude(username=settings.ANONYMOUS_USER_NAME)\n+            .annotate(\n+                full_name=Concat(\n+                    \"first_name\",\n+                    Value(\" \"),\n+                    \"last_name\",\n+                    output_field=CharField(),\n+                )\n+            )\n+            .select_related(\"verification\", \"user_profile\")\n         )\n \n         if self.q:\n-            qs = qs.filter(username__istartswith=self.q)\n+            qs = qs.filter(\n+                Q(username__icontains=self.q)\n+                | Q(email__icontains=self.q)\n+                | Q(full_name__icontains=self.q)\n+                | Q(verification__email__icontains=self.q)\n+            )\n \n         return qs\n+\n+    def get_result_label(self, result):\n+\n+        try:\n+            is_verified = result.verification.is_verified\n+        except Verification.DoesNotExist:\n+            is_verified = False\n+\n+        if is_verified:\n+            return format_html(\n+                '<img src=\"{}\" width =\"20\" height =\"20\" style=\"vertical-align:top\"> '\n+                \"&nbsp; <b>{}</b> &nbsp; {} &nbsp;\"\n+                '<i class=\"fas fa-user-check text-success\" '\n+                'title=\"Verified email address at {}\">',\n+                result.user_profile.get_mugshot_url(),\n+                result.get_username(),\n+                result.get_full_name().title(),\n+                result.verification.email.split(\"@\")[1],\n+            )\n+        else:\n+            return format_html(\n+                '<img src=\"{}\" width =\"20\" height =\"20\" style=\"vertical-align:top\"> '\n+                \"&nbsp; <b>{}</b> &nbsp; {}\",\n+                result.user_profile.get_mugshot_url(),\n+                result.get_username(),\n+                result.get_full_name().title(),\n+            )\n", "issue": "Improve the user search and selection form\nIn #1684 the user selection for groups was consolidated, so now it would be good to improve it. The form uses a ModelSelect2 widget (https://github.com/comic/grand-challenge.org/blob/master/app/grandchallenge/groups/forms.py#L19) that calls an autocomplete view that only allows filtering by username (https://github.com/comic/grand-challenge.org/blob/master/app/grandchallenge/groups/views.py#L67). \r\n\r\nTop priority is to extend the filter:\r\n- Switch to filtering with `icontains` rather than `istartswith`\r\n- Allow filtering on the users username, email address, full name, OR verification email address. To do this use Django `Q` objects (https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q-objects).\r\n\r\nNext, is to improve the look of the widget, at the moment you just get a string of the username:\r\n![image](https://user-images.githubusercontent.com/12661555/105184656-e1834880-5b2f-11eb-9ac7-36c9e2fcff22.png)\r\n\r\nOn github it's much nicer\r\n![image](https://user-images.githubusercontent.com/12661555/105184861-214a3000-5b30-11eb-9fe5-24aa1c4613a4.png)\r\n\r\nExtend this to\r\n- Include the users full name in the string\r\n- Show whether the user is verified or not in the string, and if so, what is the domain of their verification address (see https://github.com/comic/grand-challenge.org/blob/master/app/grandchallenge/profiles/templatetags/profiles.py#L16)\r\n- Stretch goal: Show the users mugshot\r\n\n", "before_files": [{"content": "from dal import autocomplete\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.core.exceptions import ValidationError\nfrom django.forms import ChoiceField, Form, HiddenInput, ModelChoiceField\nfrom guardian.utils import get_anonymous_user\n\nfrom grandchallenge.core.forms import SaveFormInitMixin\n\n\nclass UserGroupForm(SaveFormInitMixin, Form):\n    ADD = \"ADD\"\n    REMOVE = \"REMOVE\"\n    CHOICES = ((ADD, \"Add\"), (REMOVE, \"Remove\"))\n    user = ModelChoiceField(\n        queryset=get_user_model().objects.all().order_by(\"username\"),\n        help_text=\"Select a user that will be added to the group\",\n        required=True,\n        widget=autocomplete.ModelSelect2(\n            url=\"users-autocomplete\",\n            attrs={\n                \"data-placeholder\": \"Search for a user ...\",\n                \"data-minimum-input-length\": 3,\n                \"data-theme\": settings.CRISPY_TEMPLATE_PACK,\n            },\n        ),\n    )\n    action = ChoiceField(\n        choices=CHOICES, required=True, widget=HiddenInput(), initial=ADD\n    )\n\n    def clean_user(self):\n        user = self.cleaned_data[\"user\"]\n        if user == get_anonymous_user():\n            raise ValidationError(\"You cannot add this user!\")\n        return user\n\n    def add_or_remove_user(self, *, obj):\n        if self.cleaned_data[\"action\"] == self.ADD:\n            getattr(obj, f\"add_{self.role}\")(self.cleaned_data[\"user\"])\n        elif self.cleaned_data[\"action\"] == self.REMOVE:\n            getattr(obj, f\"remove_{self.role}\")(self.cleaned_data[\"user\"])\n\n\nclass EditorsForm(UserGroupForm):\n    role = \"editor\"\n\n\nclass MembersForm(UserGroupForm):\n    role = \"member\"\n\n\nclass UsersForm(UserGroupForm):\n    role = \"user\"\n", "path": "app/grandchallenge/groups/forms.py"}, {"content": "from dal import autocomplete\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.views.generic import FormView\nfrom guardian.mixins import (\n    LoginRequiredMixin,\n    PermissionRequiredMixin as ObjectPermissionRequiredMixin,\n)\nfrom guardian.shortcuts import get_objects_for_user\n\n\nclass UserGroupUpdateMixin(\n    LoginRequiredMixin,\n    ObjectPermissionRequiredMixin,\n    SuccessMessageMixin,\n    FormView,\n):\n    raise_exception = True\n\n    def get_permission_object(self):\n        return self.obj\n\n    @property\n    def obj(self):\n        raise NotImplementedError\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update({\"object\": self.obj, \"role\": self.get_form().role})\n        return context\n\n    def get_success_url(self):\n        return self.obj.get_absolute_url()\n\n    def form_valid(self, form):\n        form.add_or_remove_user(obj=self.obj)\n        return super().form_valid(form)\n\n\nclass UserAutocomplete(\n    LoginRequiredMixin, UserPassesTestMixin, autocomplete.Select2QuerySetView\n):\n    def test_func(self):\n        allowed_perms = [\n            \"algorithms.change_algorithm\",\n            \"organizations.change_organization\",\n            \"archives.change_archive\",\n            \"reader_studies.change_readerstudy\",\n            \"workstations.change_workstation\",\n            \"algorithms.change_job\",\n        ]\n        return any(\n            get_objects_for_user(user=self.request.user, perms=perm,).exists()\n            for perm in allowed_perms\n        )\n\n    def get_queryset(self):\n        qs = (\n            get_user_model()\n            .objects.order_by(\"username\")\n            .exclude(username=settings.ANONYMOUS_USER_NAME)\n        )\n\n        if self.q:\n            qs = qs.filter(username__istartswith=self.q)\n\n        return qs\n", "path": "app/grandchallenge/groups/views.py"}], "after_files": [{"content": "from dal import autocomplete\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.core.exceptions import ValidationError\nfrom django.forms import ChoiceField, Form, HiddenInput, ModelChoiceField\nfrom guardian.utils import get_anonymous_user\n\nfrom grandchallenge.core.forms import SaveFormInitMixin\n\n\nclass UserGroupForm(SaveFormInitMixin, Form):\n    ADD = \"ADD\"\n    REMOVE = \"REMOVE\"\n    CHOICES = ((ADD, \"Add\"), (REMOVE, \"Remove\"))\n    user = ModelChoiceField(\n        queryset=get_user_model().objects.all().order_by(\"username\"),\n        help_text=\"Select a user that will be added to the group\",\n        required=True,\n        widget=autocomplete.ModelSelect2(\n            url=\"users-autocomplete\",\n            attrs={\n                \"data-placeholder\": \"Search for a user ...\",\n                \"data-minimum-input-length\": 3,\n                \"data-theme\": settings.CRISPY_TEMPLATE_PACK,\n                \"data-html\": True,\n            },\n        ),\n    )\n    action = ChoiceField(\n        choices=CHOICES, required=True, widget=HiddenInput(), initial=ADD\n    )\n\n    def clean_user(self):\n        user = self.cleaned_data[\"user\"]\n        if user == get_anonymous_user():\n            raise ValidationError(\"You cannot add this user!\")\n        return user\n\n    def add_or_remove_user(self, *, obj):\n        if self.cleaned_data[\"action\"] == self.ADD:\n            getattr(obj, f\"add_{self.role}\")(self.cleaned_data[\"user\"])\n        elif self.cleaned_data[\"action\"] == self.REMOVE:\n            getattr(obj, f\"remove_{self.role}\")(self.cleaned_data[\"user\"])\n\n\nclass EditorsForm(UserGroupForm):\n    role = \"editor\"\n\n\nclass MembersForm(UserGroupForm):\n    role = \"member\"\n\n\nclass UsersForm(UserGroupForm):\n    role = \"user\"\n", "path": "app/grandchallenge/groups/forms.py"}, {"content": "from dal import autocomplete\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.mixins import UserPassesTestMixin\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.db.models import CharField, Q, Value\nfrom django.db.models.functions import Concat\nfrom django.utils.html import format_html\nfrom django.views.generic import FormView\nfrom guardian.mixins import (\n    LoginRequiredMixin,\n    PermissionRequiredMixin as ObjectPermissionRequiredMixin,\n)\nfrom guardian.shortcuts import get_objects_for_user\n\nfrom grandchallenge.verifications.models import Verification\n\n\nclass UserGroupUpdateMixin(\n    LoginRequiredMixin,\n    ObjectPermissionRequiredMixin,\n    SuccessMessageMixin,\n    FormView,\n):\n    raise_exception = True\n\n    def get_permission_object(self):\n        return self.obj\n\n    @property\n    def obj(self):\n        raise NotImplementedError\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context.update({\"object\": self.obj, \"role\": self.get_form().role})\n        return context\n\n    def get_success_url(self):\n        return self.obj.get_absolute_url()\n\n    def form_valid(self, form):\n        form.add_or_remove_user(obj=self.obj)\n        return super().form_valid(form)\n\n\nclass UserAutocomplete(\n    LoginRequiredMixin, UserPassesTestMixin, autocomplete.Select2QuerySetView\n):\n    def test_func(self):\n        allowed_perms = [\n            \"algorithms.change_algorithm\",\n            \"organizations.change_organization\",\n            \"archives.change_archive\",\n            \"reader_studies.change_readerstudy\",\n            \"workstations.change_workstation\",\n            \"algorithms.change_job\",\n        ]\n        # TODO reduce number of queries\n        return any(\n            get_objects_for_user(user=self.request.user, perms=perm,).exists()\n            for perm in allowed_perms\n        )\n\n    def get_queryset(self):\n        qs = (\n            get_user_model()\n            .objects.order_by(\"username\")\n            .exclude(username=settings.ANONYMOUS_USER_NAME)\n            .annotate(\n                full_name=Concat(\n                    \"first_name\",\n                    Value(\" \"),\n                    \"last_name\",\n                    output_field=CharField(),\n                )\n            )\n            .select_related(\"verification\", \"user_profile\")\n        )\n\n        if self.q:\n            qs = qs.filter(\n                Q(username__icontains=self.q)\n                | Q(email__icontains=self.q)\n                | Q(full_name__icontains=self.q)\n                | Q(verification__email__icontains=self.q)\n            )\n\n        return qs\n\n    def get_result_label(self, result):\n\n        try:\n            is_verified = result.verification.is_verified\n        except Verification.DoesNotExist:\n            is_verified = False\n\n        if is_verified:\n            return format_html(\n                '<img src=\"{}\" width =\"20\" height =\"20\" style=\"vertical-align:top\"> '\n                \"&nbsp; <b>{}</b> &nbsp; {} &nbsp;\"\n                '<i class=\"fas fa-user-check text-success\" '\n                'title=\"Verified email address at {}\">',\n                result.user_profile.get_mugshot_url(),\n                result.get_username(),\n                result.get_full_name().title(),\n                result.verification.email.split(\"@\")[1],\n            )\n        else:\n            return format_html(\n                '<img src=\"{}\" width =\"20\" height =\"20\" style=\"vertical-align:top\"> '\n                \"&nbsp; <b>{}</b> &nbsp; {}\",\n                result.user_profile.get_mugshot_url(),\n                result.get_username(),\n                result.get_full_name().title(),\n            )\n", "path": "app/grandchallenge/groups/views.py"}]}