We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Reset password doesn't work if user doesn't have verified/primary email address.
**Describe the bug**

Password reset is not working. I get the email, follow the link, enter the password twice, submit the form, and get back a page with no error messages. But the page title is "Error processing form -- Log in".

**Expected behavior**

I expected a positive confirmation that my password was reset, and to be able to login with the new password. Neither one happens.

**To Reproduce**

1. Start at `https://pypi.org/account/login/`
2. Click "Forgot password?" link
3. Enter the email address associated with my PyPI account (gerg.ward at gmail dot com)
4. Check my inbox and the email is already there.
5. Click the link: `https://pypi.org/account/reset-password/?token=.eJw1...`
6. Generate a new password outside my browser.
7. Copy/paste the new password into both password fields; text changes to "Passwords match" and the "Reset password" button is enabled.
8. Click "Reset password"
9. Receive the same "Reset password" form again, but with positive confirmation and no error messages. Only the page title gives a clue: "Error processing form –  Reset your password · PyPI". When I try to login with the new password (again, copy/pasted), it does not work.
 
I'll attach HTML and screenshot.

**My Platform**

Firefox 87.0 on Ubuntu 20.04.2.

Same behaviour with Chromium.

No HTTP proxy.

**Additional context**


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `warehouse/admin/views/users.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 import shlex
14 
15 import wtforms
16 import wtforms.fields.html5
17 
18 from paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage
19 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther
20 from pyramid.view import view_config
21 from sqlalchemy import or_
22 from sqlalchemy.orm import joinedload
23 from sqlalchemy.orm.exc import NoResultFound
24 
25 from warehouse import forms
26 from warehouse.accounts.interfaces import IUserService
27 from warehouse.accounts.models import DisableReason, Email, User
28 from warehouse.email import send_password_compromised_email
29 from warehouse.packaging.models import JournalEntry, Project, Role
30 from warehouse.utils.paginate import paginate_url_factory
31 
32 
33 @view_config(
34     route_name="admin.user.list",
35     renderer="admin/users/list.html",
36     permission="moderator",
37     uses_session=True,
38 )
39 def user_list(request):
40     q = request.params.get("q")
41 
42     try:
43         page_num = int(request.params.get("page", 1))
44     except ValueError:
45         raise HTTPBadRequest("'page' must be an integer.") from None
46 
47     users_query = request.db.query(User).order_by(User.username)
48 
49     if q:
50         terms = shlex.split(q)
51 
52         filters = []
53         for term in terms:
54             if ":" in term:
55                 field, value = term.split(":", 1)
56                 if field.lower() == "email":
57                     filters.append(User.emails.any(Email.email.ilike(value)))
58             else:
59                 filters.append(User.username.ilike(term))
60 
61         users_query = users_query.filter(or_(*filters))
62 
63     users = SQLAlchemyORMPage(
64         users_query,
65         page=page_num,
66         items_per_page=25,
67         url_maker=paginate_url_factory(request),
68     )
69 
70     return {"users": users, "query": q}
71 
72 
73 class EmailForm(forms.Form):
74 
75     email = wtforms.fields.html5.EmailField(
76         validators=[wtforms.validators.DataRequired()]
77     )
78     primary = wtforms.fields.BooleanField()
79     verified = wtforms.fields.BooleanField()
80     public = wtforms.fields.BooleanField()
81 
82 
83 class UserForm(forms.Form):
84 
85     name = wtforms.StringField(
86         validators=[wtforms.validators.Optional(), wtforms.validators.Length(max=100)]
87     )
88 
89     is_active = wtforms.fields.BooleanField()
90     is_superuser = wtforms.fields.BooleanField()
91     is_moderator = wtforms.fields.BooleanField()
92     is_psf_staff = wtforms.fields.BooleanField()
93 
94     prohibit_password_reset = wtforms.fields.BooleanField()
95 
96     emails = wtforms.fields.FieldList(wtforms.fields.FormField(EmailForm))
97 
98 
99 @view_config(
100     route_name="admin.user.detail",
101     renderer="admin/users/detail.html",
102     permission="moderator",
103     request_method="GET",
104     uses_session=True,
105     require_csrf=True,
106     require_methods=False,
107 )
108 @view_config(
109     route_name="admin.user.detail",
110     renderer="admin/users/detail.html",
111     permission="admin",
112     request_method="POST",
113     uses_session=True,
114     require_csrf=True,
115     require_methods=False,
116 )
117 def user_detail(request):
118     try:
119         user = (
120             request.db.query(User).filter(User.id == request.matchdict["user_id"]).one()
121         )
122     except NoResultFound:
123         raise HTTPNotFound
124 
125     roles = (
126         request.db.query(Role)
127         .join(User)
128         .filter(Role.user == user)
129         .order_by(Role.role_name, Role.project_id)
130         .all()
131     )
132 
133     form = UserForm(request.POST if request.method == "POST" else None, user)
134 
135     if request.method == "POST" and form.validate():
136         form.populate_obj(user)
137         return HTTPSeeOther(location=request.current_route_path())
138 
139     return {"user": user, "form": form, "roles": roles, "add_email_form": EmailForm()}
140 
141 
142 @view_config(
143     route_name="admin.user.add_email",
144     require_methods=["POST"],
145     permission="admin",
146     uses_session=True,
147     require_csrf=True,
148 )
149 def user_add_email(request):
150     user = request.db.query(User).get(request.matchdict["user_id"])
151     form = EmailForm(request.POST)
152 
153     if form.validate():
154         email = Email(
155             email=form.email.data,
156             user=user,
157             primary=form.primary.data,
158             verified=form.verified.data,
159             public=form.public.data,
160         )
161         request.db.add(email)
162         request.session.flash(
163             f"Added email for user {user.username!r}", queue="success"
164         )
165 
166     return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
167 
168 
169 @view_config(
170     route_name="admin.user.delete",
171     require_methods=["POST"],
172     permission="admin",
173     uses_session=True,
174     require_csrf=True,
175 )
176 def user_delete(request):
177     user = request.db.query(User).get(request.matchdict["user_id"])
178 
179     if user.username != request.params.get("username"):
180         request.session.flash("Wrong confirmation input", queue="error")
181         return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
182 
183     # Delete all the user's projects
184     projects = request.db.query(Project).filter(
185         Project.name.in_(
186             request.db.query(Project.name)
187             .join(Role.project)
188             .filter(Role.user == user)
189             .subquery()
190         )
191     )
192     for project in projects:
193         request.db.add(
194             JournalEntry(
195                 name=project.name,
196                 action="remove project",
197                 submitted_by=request.user,
198                 submitted_from=request.remote_addr,
199             )
200         )
201     projects.delete(synchronize_session=False)
202 
203     # Update all journals to point to `deleted-user` instead
204     deleted_user = request.db.query(User).filter(User.username == "deleted-user").one()
205 
206     journals = (
207         request.db.query(JournalEntry)
208         .options(joinedload("submitted_by"))
209         .filter(JournalEntry.submitted_by == user)
210         .all()
211     )
212 
213     for journal in journals:
214         journal.submitted_by = deleted_user
215 
216     # Delete the user
217     request.db.delete(user)
218     request.db.add(
219         JournalEntry(
220             name=f"user:{user.username}",
221             action="nuke user",
222             submitted_by=request.user,
223             submitted_from=request.remote_addr,
224         )
225     )
226     request.session.flash(f"Nuked user {user.username!r}", queue="success")
227     return HTTPSeeOther(request.route_path("admin.user.list"))
228 
229 
230 @view_config(
231     route_name="admin.user.reset_password",
232     require_methods=["POST"],
233     permission="admin",
234     has_translations=True,
235     uses_session=True,
236     require_csrf=True,
237 )
238 def user_reset_password(request):
239     user = request.db.query(User).get(request.matchdict["user_id"])
240 
241     if user.username != request.params.get("username"):
242         request.session.flash("Wrong confirmation input", queue="error")
243         return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
244 
245     login_service = request.find_service(IUserService, context=None)
246     send_password_compromised_email(request, user)
247     login_service.disable_password(user.id, reason=DisableReason.CompromisedPassword)
248 
249     request.session.flash(f"Reset password for {user.username!r}", queue="success")
250     return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
251
```
--- 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/warehouse/admin/views/users.py b/warehouse/admin/views/users.py
--- a/warehouse/admin/views/users.py
+++ b/warehouse/admin/views/users.py
@@ -14,6 +14,7 @@
 
 import wtforms
 import wtforms.fields.html5
+import wtforms.validators
 
 from paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage
 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther
@@ -95,6 +96,14 @@
 
     emails = wtforms.fields.FieldList(wtforms.fields.FormField(EmailForm))
 
+    def validate_emails(self, field):
+        # If there's no email on the account, it's ok. Otherwise, ensure
+        # we have 1 primary email.
+        if field.data and len([1 for email in field.data if email["primary"]]) != 1:
+            raise wtforms.validators.ValidationError(
+                "There must be exactly one primary email"
+            )
+
 
 @view_config(
     route_name="admin.user.detail",
@@ -134,6 +143,7 @@
 
     if request.method == "POST" and form.validate():
         form.populate_obj(user)
+        request.session.flash(f"User {user.username!r} updated", queue="success")
         return HTTPSeeOther(location=request.current_route_path())
 
     return {"user": user, "form": form, "roles": roles, "add_email_form": EmailForm()}
@@ -151,6 +161,11 @@
     form = EmailForm(request.POST)
 
     if form.validate():
+
+        if form.primary.data:
+            for other in user.emails:
+                other.primary = False
+
         email = Email(
             email=form.email.data,
             user=user,

{"golden_diff": "diff --git a/warehouse/admin/views/users.py b/warehouse/admin/views/users.py\n--- a/warehouse/admin/views/users.py\n+++ b/warehouse/admin/views/users.py\n@@ -14,6 +14,7 @@\n \n import wtforms\n import wtforms.fields.html5\n+import wtforms.validators\n \n from paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage\n from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther\n@@ -95,6 +96,14 @@\n \n     emails = wtforms.fields.FieldList(wtforms.fields.FormField(EmailForm))\n \n+    def validate_emails(self, field):\n+        # If there's no email on the account, it's ok. Otherwise, ensure\n+        # we have 1 primary email.\n+        if field.data and len([1 for email in field.data if email[\"primary\"]]) != 1:\n+            raise wtforms.validators.ValidationError(\n+                \"There must be exactly one primary email\"\n+            )\n+\n \n @view_config(\n     route_name=\"admin.user.detail\",\n@@ -134,6 +143,7 @@\n \n     if request.method == \"POST\" and form.validate():\n         form.populate_obj(user)\n+        request.session.flash(f\"User {user.username!r} updated\", queue=\"success\")\n         return HTTPSeeOther(location=request.current_route_path())\n \n     return {\"user\": user, \"form\": form, \"roles\": roles, \"add_email_form\": EmailForm()}\n@@ -151,6 +161,11 @@\n     form = EmailForm(request.POST)\n \n     if form.validate():\n+\n+        if form.primary.data:\n+            for other in user.emails:\n+                other.primary = False\n+\n         email = Email(\n             email=form.email.data,\n             user=user,\n", "issue": "Reset password doesn't work if user doesn't have verified/primary email address.\n**Describe the bug**\r\n\r\nPassword reset is not working. I get the email, follow the link, enter the password twice, submit the form, and get back a page with no error messages. But the page title is \"Error processing form -- Log in\".\r\n\r\n**Expected behavior**\r\n\r\nI expected a positive confirmation that my password was reset, and to be able to login with the new password. Neither one happens.\r\n\r\n**To Reproduce**\r\n\r\n1. Start at `https://pypi.org/account/login/`\r\n2. Click \"Forgot password?\" link\r\n3. Enter the email address associated with my PyPI account (gerg.ward at gmail dot com)\r\n4. Check my inbox and the email is already there.\r\n5. Click the link: `https://pypi.org/account/reset-password/?token=.eJw1...`\r\n6. Generate a new password outside my browser.\r\n7. Copy/paste the new password into both password fields; text changes to \"Passwords match\" and the \"Reset password\" button is enabled.\r\n8. Click \"Reset password\"\r\n9. Receive the same \"Reset password\" form again, but with positive confirmation and no error messages. Only the page title gives a clue: \"Error processing form \u2013  Reset your password \u00b7 PyPI\". When I try to login with the new password (again, copy/pasted), it does not work.\r\n \r\nI'll attach HTML and screenshot.\r\n\r\n**My Platform**\r\n\r\nFirefox 87.0 on Ubuntu 20.04.2.\r\n\r\nSame behaviour with Chromium.\r\n\r\nNo HTTP proxy.\r\n\r\n**Additional context**\r\n\n", "before_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport shlex\n\nimport wtforms\nimport wtforms.fields.html5\n\nfrom paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage\nfrom pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther\nfrom pyramid.view import view_config\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import joinedload\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom warehouse import forms\nfrom warehouse.accounts.interfaces import IUserService\nfrom warehouse.accounts.models import DisableReason, Email, User\nfrom warehouse.email import send_password_compromised_email\nfrom warehouse.packaging.models import JournalEntry, Project, Role\nfrom warehouse.utils.paginate import paginate_url_factory\n\n\n@view_config(\n    route_name=\"admin.user.list\",\n    renderer=\"admin/users/list.html\",\n    permission=\"moderator\",\n    uses_session=True,\n)\ndef user_list(request):\n    q = request.params.get(\"q\")\n\n    try:\n        page_num = int(request.params.get(\"page\", 1))\n    except ValueError:\n        raise HTTPBadRequest(\"'page' must be an integer.\") from None\n\n    users_query = request.db.query(User).order_by(User.username)\n\n    if q:\n        terms = shlex.split(q)\n\n        filters = []\n        for term in terms:\n            if \":\" in term:\n                field, value = term.split(\":\", 1)\n                if field.lower() == \"email\":\n                    filters.append(User.emails.any(Email.email.ilike(value)))\n            else:\n                filters.append(User.username.ilike(term))\n\n        users_query = users_query.filter(or_(*filters))\n\n    users = SQLAlchemyORMPage(\n        users_query,\n        page=page_num,\n        items_per_page=25,\n        url_maker=paginate_url_factory(request),\n    )\n\n    return {\"users\": users, \"query\": q}\n\n\nclass EmailForm(forms.Form):\n\n    email = wtforms.fields.html5.EmailField(\n        validators=[wtforms.validators.DataRequired()]\n    )\n    primary = wtforms.fields.BooleanField()\n    verified = wtforms.fields.BooleanField()\n    public = wtforms.fields.BooleanField()\n\n\nclass UserForm(forms.Form):\n\n    name = wtforms.StringField(\n        validators=[wtforms.validators.Optional(), wtforms.validators.Length(max=100)]\n    )\n\n    is_active = wtforms.fields.BooleanField()\n    is_superuser = wtforms.fields.BooleanField()\n    is_moderator = wtforms.fields.BooleanField()\n    is_psf_staff = wtforms.fields.BooleanField()\n\n    prohibit_password_reset = wtforms.fields.BooleanField()\n\n    emails = wtforms.fields.FieldList(wtforms.fields.FormField(EmailForm))\n\n\n@view_config(\n    route_name=\"admin.user.detail\",\n    renderer=\"admin/users/detail.html\",\n    permission=\"moderator\",\n    request_method=\"GET\",\n    uses_session=True,\n    require_csrf=True,\n    require_methods=False,\n)\n@view_config(\n    route_name=\"admin.user.detail\",\n    renderer=\"admin/users/detail.html\",\n    permission=\"admin\",\n    request_method=\"POST\",\n    uses_session=True,\n    require_csrf=True,\n    require_methods=False,\n)\ndef user_detail(request):\n    try:\n        user = (\n            request.db.query(User).filter(User.id == request.matchdict[\"user_id\"]).one()\n        )\n    except NoResultFound:\n        raise HTTPNotFound\n\n    roles = (\n        request.db.query(Role)\n        .join(User)\n        .filter(Role.user == user)\n        .order_by(Role.role_name, Role.project_id)\n        .all()\n    )\n\n    form = UserForm(request.POST if request.method == \"POST\" else None, user)\n\n    if request.method == \"POST\" and form.validate():\n        form.populate_obj(user)\n        return HTTPSeeOther(location=request.current_route_path())\n\n    return {\"user\": user, \"form\": form, \"roles\": roles, \"add_email_form\": EmailForm()}\n\n\n@view_config(\n    route_name=\"admin.user.add_email\",\n    require_methods=[\"POST\"],\n    permission=\"admin\",\n    uses_session=True,\n    require_csrf=True,\n)\ndef user_add_email(request):\n    user = request.db.query(User).get(request.matchdict[\"user_id\"])\n    form = EmailForm(request.POST)\n\n    if form.validate():\n        email = Email(\n            email=form.email.data,\n            user=user,\n            primary=form.primary.data,\n            verified=form.verified.data,\n            public=form.public.data,\n        )\n        request.db.add(email)\n        request.session.flash(\n            f\"Added email for user {user.username!r}\", queue=\"success\"\n        )\n\n    return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n\n\n@view_config(\n    route_name=\"admin.user.delete\",\n    require_methods=[\"POST\"],\n    permission=\"admin\",\n    uses_session=True,\n    require_csrf=True,\n)\ndef user_delete(request):\n    user = request.db.query(User).get(request.matchdict[\"user_id\"])\n\n    if user.username != request.params.get(\"username\"):\n        request.session.flash(\"Wrong confirmation input\", queue=\"error\")\n        return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n\n    # Delete all the user's projects\n    projects = request.db.query(Project).filter(\n        Project.name.in_(\n            request.db.query(Project.name)\n            .join(Role.project)\n            .filter(Role.user == user)\n            .subquery()\n        )\n    )\n    for project in projects:\n        request.db.add(\n            JournalEntry(\n                name=project.name,\n                action=\"remove project\",\n                submitted_by=request.user,\n                submitted_from=request.remote_addr,\n            )\n        )\n    projects.delete(synchronize_session=False)\n\n    # Update all journals to point to `deleted-user` instead\n    deleted_user = request.db.query(User).filter(User.username == \"deleted-user\").one()\n\n    journals = (\n        request.db.query(JournalEntry)\n        .options(joinedload(\"submitted_by\"))\n        .filter(JournalEntry.submitted_by == user)\n        .all()\n    )\n\n    for journal in journals:\n        journal.submitted_by = deleted_user\n\n    # Delete the user\n    request.db.delete(user)\n    request.db.add(\n        JournalEntry(\n            name=f\"user:{user.username}\",\n            action=\"nuke user\",\n            submitted_by=request.user,\n            submitted_from=request.remote_addr,\n        )\n    )\n    request.session.flash(f\"Nuked user {user.username!r}\", queue=\"success\")\n    return HTTPSeeOther(request.route_path(\"admin.user.list\"))\n\n\n@view_config(\n    route_name=\"admin.user.reset_password\",\n    require_methods=[\"POST\"],\n    permission=\"admin\",\n    has_translations=True,\n    uses_session=True,\n    require_csrf=True,\n)\ndef user_reset_password(request):\n    user = request.db.query(User).get(request.matchdict[\"user_id\"])\n\n    if user.username != request.params.get(\"username\"):\n        request.session.flash(\"Wrong confirmation input\", queue=\"error\")\n        return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n\n    login_service = request.find_service(IUserService, context=None)\n    send_password_compromised_email(request, user)\n    login_service.disable_password(user.id, reason=DisableReason.CompromisedPassword)\n\n    request.session.flash(f\"Reset password for {user.username!r}\", queue=\"success\")\n    return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n", "path": "warehouse/admin/views/users.py"}], "after_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport shlex\n\nimport wtforms\nimport wtforms.fields.html5\nimport wtforms.validators\n\nfrom paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage\nfrom pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther\nfrom pyramid.view import view_config\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import joinedload\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom warehouse import forms\nfrom warehouse.accounts.interfaces import IUserService\nfrom warehouse.accounts.models import DisableReason, Email, User\nfrom warehouse.email import send_password_compromised_email\nfrom warehouse.packaging.models import JournalEntry, Project, Role\nfrom warehouse.utils.paginate import paginate_url_factory\n\n\n@view_config(\n    route_name=\"admin.user.list\",\n    renderer=\"admin/users/list.html\",\n    permission=\"moderator\",\n    uses_session=True,\n)\ndef user_list(request):\n    q = request.params.get(\"q\")\n\n    try:\n        page_num = int(request.params.get(\"page\", 1))\n    except ValueError:\n        raise HTTPBadRequest(\"'page' must be an integer.\") from None\n\n    users_query = request.db.query(User).order_by(User.username)\n\n    if q:\n        terms = shlex.split(q)\n\n        filters = []\n        for term in terms:\n            if \":\" in term:\n                field, value = term.split(\":\", 1)\n                if field.lower() == \"email\":\n                    filters.append(User.emails.any(Email.email.ilike(value)))\n            else:\n                filters.append(User.username.ilike(term))\n\n        users_query = users_query.filter(or_(*filters))\n\n    users = SQLAlchemyORMPage(\n        users_query,\n        page=page_num,\n        items_per_page=25,\n        url_maker=paginate_url_factory(request),\n    )\n\n    return {\"users\": users, \"query\": q}\n\n\nclass EmailForm(forms.Form):\n\n    email = wtforms.fields.html5.EmailField(\n        validators=[wtforms.validators.DataRequired()]\n    )\n    primary = wtforms.fields.BooleanField()\n    verified = wtforms.fields.BooleanField()\n    public = wtforms.fields.BooleanField()\n\n\nclass UserForm(forms.Form):\n\n    name = wtforms.StringField(\n        validators=[wtforms.validators.Optional(), wtforms.validators.Length(max=100)]\n    )\n\n    is_active = wtforms.fields.BooleanField()\n    is_superuser = wtforms.fields.BooleanField()\n    is_moderator = wtforms.fields.BooleanField()\n    is_psf_staff = wtforms.fields.BooleanField()\n\n    prohibit_password_reset = wtforms.fields.BooleanField()\n\n    emails = wtforms.fields.FieldList(wtforms.fields.FormField(EmailForm))\n\n    def validate_emails(self, field):\n        # If there's no email on the account, it's ok. Otherwise, ensure\n        # we have 1 primary email.\n        if field.data and len([1 for email in field.data if email[\"primary\"]]) != 1:\n            raise wtforms.validators.ValidationError(\n                \"There must be exactly one primary email\"\n            )\n\n\n@view_config(\n    route_name=\"admin.user.detail\",\n    renderer=\"admin/users/detail.html\",\n    permission=\"moderator\",\n    request_method=\"GET\",\n    uses_session=True,\n    require_csrf=True,\n    require_methods=False,\n)\n@view_config(\n    route_name=\"admin.user.detail\",\n    renderer=\"admin/users/detail.html\",\n    permission=\"admin\",\n    request_method=\"POST\",\n    uses_session=True,\n    require_csrf=True,\n    require_methods=False,\n)\ndef user_detail(request):\n    try:\n        user = (\n            request.db.query(User).filter(User.id == request.matchdict[\"user_id\"]).one()\n        )\n    except NoResultFound:\n        raise HTTPNotFound\n\n    roles = (\n        request.db.query(Role)\n        .join(User)\n        .filter(Role.user == user)\n        .order_by(Role.role_name, Role.project_id)\n        .all()\n    )\n\n    form = UserForm(request.POST if request.method == \"POST\" else None, user)\n\n    if request.method == \"POST\" and form.validate():\n        form.populate_obj(user)\n        request.session.flash(f\"User {user.username!r} updated\", queue=\"success\")\n        return HTTPSeeOther(location=request.current_route_path())\n\n    return {\"user\": user, \"form\": form, \"roles\": roles, \"add_email_form\": EmailForm()}\n\n\n@view_config(\n    route_name=\"admin.user.add_email\",\n    require_methods=[\"POST\"],\n    permission=\"admin\",\n    uses_session=True,\n    require_csrf=True,\n)\ndef user_add_email(request):\n    user = request.db.query(User).get(request.matchdict[\"user_id\"])\n    form = EmailForm(request.POST)\n\n    if form.validate():\n\n        if form.primary.data:\n            for other in user.emails:\n                other.primary = False\n\n        email = Email(\n            email=form.email.data,\n            user=user,\n            primary=form.primary.data,\n            verified=form.verified.data,\n            public=form.public.data,\n        )\n        request.db.add(email)\n        request.session.flash(\n            f\"Added email for user {user.username!r}\", queue=\"success\"\n        )\n\n    return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n\n\n@view_config(\n    route_name=\"admin.user.delete\",\n    require_methods=[\"POST\"],\n    permission=\"admin\",\n    uses_session=True,\n    require_csrf=True,\n)\ndef user_delete(request):\n    user = request.db.query(User).get(request.matchdict[\"user_id\"])\n\n    if user.username != request.params.get(\"username\"):\n        request.session.flash(\"Wrong confirmation input\", queue=\"error\")\n        return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n\n    # Delete all the user's projects\n    projects = request.db.query(Project).filter(\n        Project.name.in_(\n            request.db.query(Project.name)\n            .join(Role.project)\n            .filter(Role.user == user)\n            .subquery()\n        )\n    )\n    for project in projects:\n        request.db.add(\n            JournalEntry(\n                name=project.name,\n                action=\"remove project\",\n                submitted_by=request.user,\n                submitted_from=request.remote_addr,\n            )\n        )\n    projects.delete(synchronize_session=False)\n\n    # Update all journals to point to `deleted-user` instead\n    deleted_user = request.db.query(User).filter(User.username == \"deleted-user\").one()\n\n    journals = (\n        request.db.query(JournalEntry)\n        .options(joinedload(\"submitted_by\"))\n        .filter(JournalEntry.submitted_by == user)\n        .all()\n    )\n\n    for journal in journals:\n        journal.submitted_by = deleted_user\n\n    # Delete the user\n    request.db.delete(user)\n    request.db.add(\n        JournalEntry(\n            name=f\"user:{user.username}\",\n            action=\"nuke user\",\n            submitted_by=request.user,\n            submitted_from=request.remote_addr,\n        )\n    )\n    request.session.flash(f\"Nuked user {user.username!r}\", queue=\"success\")\n    return HTTPSeeOther(request.route_path(\"admin.user.list\"))\n\n\n@view_config(\n    route_name=\"admin.user.reset_password\",\n    require_methods=[\"POST\"],\n    permission=\"admin\",\n    has_translations=True,\n    uses_session=True,\n    require_csrf=True,\n)\ndef user_reset_password(request):\n    user = request.db.query(User).get(request.matchdict[\"user_id\"])\n\n    if user.username != request.params.get(\"username\"):\n        request.session.flash(\"Wrong confirmation input\", queue=\"error\")\n        return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n\n    login_service = request.find_service(IUserService, context=None)\n    send_password_compromised_email(request, user)\n    login_service.disable_password(user.id, reason=DisableReason.CompromisedPassword)\n\n    request.session.flash(f\"Reset password for {user.username!r}\", queue=\"success\")\n    return HTTPSeeOther(request.route_path(\"admin.user.detail\", user_id=user.id))\n", "path": "warehouse/admin/views/users.py"}]}