We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
duplicate footer appears in package page only when not logged in
When viewing a package page (appears to apply to any package), the (blue) footer ("Get Help", "About PyPI", "Contributing to PyPI") occurs twice: once below a login prompt and a search text box that appear below the package's title (with the package details below these and the footer), and once at the bottom of the page (the latter as expected).

This behavior occurs only when logged out. In contrast, when logged in, the layout seems as intended: a search text box at the top, and the package title and details contiguously below that search box, with a single footer at the bottom.

--- 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/views.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 collections
14 
15 from pyramid.httpexceptions import (
16     HTTPException, HTTPSeeOther, HTTPMovedPermanently, HTTPNotFound,
17     HTTPBadRequest, exception_response,
18 )
19 from pyramid.exceptions import PredicateMismatch
20 from pyramid.renderers import render_to_response
21 from pyramid.response import Response
22 from pyramid.view import (
23     notfound_view_config, forbidden_view_config, exception_view_config,
24     view_config,
25 )
26 from elasticsearch_dsl import Q
27 from sqlalchemy import func
28 from sqlalchemy.orm import aliased, joinedload
29 from sqlalchemy.sql import exists
30 
31 from warehouse.accounts import REDIRECT_FIELD_NAME
32 from warehouse.accounts.models import User
33 from warehouse.cache.origin import origin_cache
34 from warehouse.cache.http import cache_control
35 from warehouse.classifiers.models import Classifier
36 from warehouse.packaging.models import (
37     Project, Release, File, release_classifiers,
38 )
39 from warehouse.utils.row_counter import RowCount
40 from warehouse.utils.paginate import ElasticsearchPage, paginate_url_factory
41 
42 
43 SEARCH_FIELDS = [
44     "author", "author_email", "description", "download_url", "home_page",
45     "keywords", "license", "maintainer", "maintainer_email", "normalized_name",
46     "platform", "summary",
47 ]
48 SEARCH_BOOSTS = {
49     "normalized_name": 10,
50     "description": 5,
51     "keywords": 5,
52     "summary": 5,
53 }
54 SEARCH_FILTER_ORDER = (
55     "Programming Language",
56     "License",
57     "Framework",
58     "Topic",
59     "Intended Audience",
60     "Environment",
61     "Operating System",
62     "Natural Language",
63     "Development Status",
64 )
65 
66 
67 # 403, 404, 410, 500,
68 
69 
70 @view_config(context=HTTPException)
71 @notfound_view_config(append_slash=HTTPMovedPermanently)
72 def httpexception_view(exc, request):
73     # This special case exists for the easter egg that appears on the 404
74     # response page. We don't generally allow youtube embeds, but we make an
75     # except for this one.
76     if isinstance(exc, HTTPNotFound):
77         request.find_service(name="csp").merge({
78             "frame-src": ["https://www.youtube-nocookie.com"],
79             "script-src": ["https://www.youtube.com", "https://s.ytimg.com"],
80         })
81     try:
82         # Lightweight version of 404 page for `/simple/`
83         if (isinstance(exc, HTTPNotFound) and
84                 request.path.startswith("/simple/")):
85             response = Response(
86                 body="404 Not Found",
87                 content_type="text/plain"
88             )
89         else:
90             response = render_to_response(
91                 "{}.html".format(exc.status_code),
92                 {},
93                 request=request,
94             )
95     except LookupError:
96         # We don't have a customized template for this error, so we'll just let
97         # the default happen instead.
98         return exc
99 
100     # Copy over the important values from our HTTPException to our new response
101     # object.
102     response.status = exc.status
103     response.headers.extend(
104         (k, v) for k, v in exc.headers.items()
105         if k not in response.headers
106     )
107 
108     return response
109 
110 
111 @forbidden_view_config()
112 @exception_view_config(PredicateMismatch)
113 def forbidden(exc, request, redirect_to="accounts.login"):
114     # If the forbidden error is because the user isn't logged in, then we'll
115     # redirect them to the log in page.
116     if request.authenticated_userid is None:
117         url = request.route_url(
118             redirect_to,
119             _query={REDIRECT_FIELD_NAME: request.path_qs},
120         )
121         return HTTPSeeOther(url)
122 
123     # If we've reached here, then the user is logged in and they are genuinely
124     # not allowed to access this page.
125     return httpexception_view(exc, request)
126 
127 
128 @view_config(
129     route_name="robots.txt",
130     renderer="robots.txt",
131     decorator=[
132         cache_control(1 * 24 * 60 * 60),         # 1 day
133         origin_cache(
134             1 * 24 * 60 * 60,                    # 1 day
135             stale_while_revalidate=6 * 60 * 60,  # 6 hours
136             stale_if_error=1 * 24 * 60 * 60,     # 1 day
137         ),
138     ],
139 )
140 def robotstxt(request):
141     request.response.content_type = "text/plain"
142     return {}
143 
144 
145 @view_config(
146     route_name="opensearch.xml",
147     renderer="opensearch.xml",
148     decorator=[
149         cache_control(1 * 24 * 60 * 60),         # 1 day
150         origin_cache(
151             1 * 24 * 60 * 60,                    # 1 day
152             stale_while_revalidate=6 * 60 * 60,  # 6 hours
153             stale_if_error=1 * 24 * 60 * 60,     # 1 day
154         )
155     ]
156 )
157 def opensearchxml(request):
158     request.response.content_type = "text/xml"
159     return {}
160 
161 
162 @view_config(
163     route_name="index",
164     renderer="index.html",
165     decorator=[
166         origin_cache(
167             1 * 60 * 60,                      # 1 hour
168             stale_while_revalidate=10 * 60,   # 10 minutes
169             stale_if_error=1 * 24 * 60 * 60,  # 1 day
170             keys=["all-projects", "trending"],
171         ),
172     ]
173 )
174 def index(request):
175     project_names = [
176         r[0] for r in (
177             request.db.query(Project.name)
178                    .order_by(Project.zscore.desc().nullslast(),
179                              func.random())
180                    .limit(5)
181                    .all())
182     ]
183     release_a = aliased(
184         Release,
185         request.db.query(Release)
186                   .distinct(Release.name)
187                   .filter(Release.name.in_(project_names))
188                   .order_by(Release.name,
189                             Release.is_prerelease.nullslast(),
190                             Release._pypi_ordering.desc())
191                   .subquery(),
192     )
193     trending_projects = (
194         request.db.query(release_a)
195                .options(joinedload(release_a.project))
196                .order_by(func.array_idx(project_names, release_a.name))
197                .all()
198     )
199 
200     latest_releases = (
201         request.db.query(Release)
202                   .options(joinedload(Release.project))
203                   .order_by(Release.created.desc())
204                   .limit(5)
205                   .all()
206     )
207 
208     counts = dict(
209         request.db.query(RowCount.table_name, RowCount.count)
210                   .filter(
211                       RowCount.table_name.in_([
212                           Project.__tablename__,
213                           Release.__tablename__,
214                           File.__tablename__,
215                           User.__tablename__,
216                       ]))
217                   .all()
218     )
219 
220     return {
221         "latest_releases": latest_releases,
222         "trending_projects": trending_projects,
223         "num_projects": counts.get(Project.__tablename__, 0),
224         "num_releases": counts.get(Release.__tablename__, 0),
225         "num_files": counts.get(File.__tablename__, 0),
226         "num_users": counts.get(User.__tablename__, 0),
227     }
228 
229 
230 @view_config(
231     route_name="search",
232     renderer="search/results.html",
233     decorator=[
234         origin_cache(
235             1 * 60 * 60,                      # 1 hour
236             stale_while_revalidate=10 * 60,   # 10 minutes
237             stale_if_error=1 * 24 * 60 * 60,  # 1 day
238             keys=["all-projects"],
239         )
240     ],
241 )
242 def search(request):
243 
244     q = request.params.get("q", '')
245 
246     if q:
247         should = []
248         for field in SEARCH_FIELDS:
249             kw = {"query": q}
250             if field in SEARCH_BOOSTS:
251                 kw["boost"] = SEARCH_BOOSTS[field]
252             should.append(Q("match", **{field: kw}))
253 
254         # Add a prefix query if ``q`` is longer than one character.
255         if len(q) > 1:
256             should.append(Q('prefix', normalized_name=q))
257 
258         query = request.es.query("dis_max", queries=should)
259         query = query.suggest("name_suggestion", q, term={"field": "name"})
260     else:
261         query = request.es.query()
262 
263     if request.params.get("o"):
264         sort_key = request.params["o"]
265         if sort_key.startswith("-"):
266             sort = {
267                 sort_key[1:]: {
268                     "order": "desc",
269                     "unmapped_type": "long",
270                 },
271             }
272         else:
273             sort = {
274                 sort_key: {
275                     "unmapped_type": "long",
276                 }
277             }
278 
279         query = query.sort(sort)
280 
281     # Require match to all specified classifiers
282     for classifier in request.params.getall("c"):
283         query = query.filter("terms", classifiers=[classifier])
284 
285     try:
286         page_num = int(request.params.get("page", 1))
287     except ValueError:
288         raise HTTPBadRequest("'page' must be an integer.")
289 
290     page = ElasticsearchPage(
291         query,
292         page=page_num,
293         url_maker=paginate_url_factory(request),
294     )
295 
296     if page.page_count and page_num > page.page_count:
297         return HTTPNotFound()
298 
299     available_filters = collections.defaultdict(list)
300 
301     classifiers_q = (
302         request.db.query(Classifier)
303         .with_entities(Classifier.classifier)
304         .filter(
305             exists([release_classifiers.c.trove_id])
306             .where(release_classifiers.c.trove_id == Classifier.id)
307         )
308         .order_by(Classifier.classifier)
309     )
310 
311     for cls in classifiers_q:
312         first, *_ = cls.classifier.split(' :: ')
313         available_filters[first].append(cls.classifier)
314 
315     def filter_key(item):
316         try:
317             return 0, SEARCH_FILTER_ORDER.index(item[0]), item[0]
318         except ValueError:
319             return 1, 0, item[0]
320 
321     return {
322         "page": page,
323         "term": q,
324         "order": request.params.get("o", ''),
325         "available_filters": sorted(available_filters.items(), key=filter_key),
326         "applied_filters": request.params.getall("c"),
327     }
328 
329 
330 @view_config(
331     route_name="includes.current-user-indicator",
332     renderer="includes/current-user-indicator.html",
333     uses_session=True,
334 )
335 def current_user_indicator(request):
336     return {}
337 
338 
339 @view_config(
340     route_name="includes.flash-messages",
341     renderer="includes/flash-messages.html",
342     uses_session=True,
343 )
344 def flash_messages(request):
345     return {}
346 
347 
348 @view_config(route_name="health", renderer="string")
349 def health(request):
350     # This will ensure that we can access the database and run queries against
351     # it without doing anything that will take a lock or block other queries.
352     request.db.execute("SELECT 1")
353 
354     # Nothing will actually check this, but it's a little nicer to have
355     # something to return besides an empty body.
356     return "OK"
357 
358 
359 @view_config(route_name="force-status")
360 def force_status(request):
361     try:
362         raise exception_response(int(request.matchdict["status"]))
363     except KeyError:
364         raise exception_response(404) from None
365
```
--- 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/views.py b/warehouse/views.py
--- a/warehouse/views.py
+++ b/warehouse/views.py
@@ -125,6 +125,14 @@
     return httpexception_view(exc, request)
 
 
+@forbidden_view_config(path_info=r"^/_includes/")
+@exception_view_config(PredicateMismatch, path_info=r"^/_includes/")
+def forbidden_include(exc, request):
+    # If the forbidden error is for a client-side-include, just return an empty
+    # response instead of redirecting
+    return Response()
+
+
 @view_config(
     route_name="robots.txt",
     renderer="robots.txt",

{"golden_diff": "diff --git a/warehouse/views.py b/warehouse/views.py\n--- a/warehouse/views.py\n+++ b/warehouse/views.py\n@@ -125,6 +125,14 @@\n     return httpexception_view(exc, request)\n \n \n+@forbidden_view_config(path_info=r\"^/_includes/\")\n+@exception_view_config(PredicateMismatch, path_info=r\"^/_includes/\")\n+def forbidden_include(exc, request):\n+    # If the forbidden error is for a client-side-include, just return an empty\n+    # response instead of redirecting\n+    return Response()\n+\n+\n @view_config(\n     route_name=\"robots.txt\",\n     renderer=\"robots.txt\",\n", "issue": "duplicate footer appears in package page only when not logged in\nWhen viewing a package page (appears to apply to any package), the (blue) footer (\"Get Help\", \"About PyPI\", \"Contributing to PyPI\") occurs twice: once below a login prompt and a search text box that appear below the package's title (with the package details below these and the footer), and once at the bottom of the page (the latter as expected).\r\n\r\nThis behavior occurs only when logged out. In contrast, when logged in, the layout seems as intended: a search text box at the top, and the package title and details contiguously below that search box, with a single footer at the bottom.\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 collections\n\nfrom pyramid.httpexceptions import (\n    HTTPException, HTTPSeeOther, HTTPMovedPermanently, HTTPNotFound,\n    HTTPBadRequest, exception_response,\n)\nfrom pyramid.exceptions import PredicateMismatch\nfrom pyramid.renderers import render_to_response\nfrom pyramid.response import Response\nfrom pyramid.view import (\n    notfound_view_config, forbidden_view_config, exception_view_config,\n    view_config,\n)\nfrom elasticsearch_dsl import Q\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import aliased, joinedload\nfrom sqlalchemy.sql import exists\n\nfrom warehouse.accounts import REDIRECT_FIELD_NAME\nfrom warehouse.accounts.models import User\nfrom warehouse.cache.origin import origin_cache\nfrom warehouse.cache.http import cache_control\nfrom warehouse.classifiers.models import Classifier\nfrom warehouse.packaging.models import (\n    Project, Release, File, release_classifiers,\n)\nfrom warehouse.utils.row_counter import RowCount\nfrom warehouse.utils.paginate import ElasticsearchPage, paginate_url_factory\n\n\nSEARCH_FIELDS = [\n    \"author\", \"author_email\", \"description\", \"download_url\", \"home_page\",\n    \"keywords\", \"license\", \"maintainer\", \"maintainer_email\", \"normalized_name\",\n    \"platform\", \"summary\",\n]\nSEARCH_BOOSTS = {\n    \"normalized_name\": 10,\n    \"description\": 5,\n    \"keywords\": 5,\n    \"summary\": 5,\n}\nSEARCH_FILTER_ORDER = (\n    \"Programming Language\",\n    \"License\",\n    \"Framework\",\n    \"Topic\",\n    \"Intended Audience\",\n    \"Environment\",\n    \"Operating System\",\n    \"Natural Language\",\n    \"Development Status\",\n)\n\n\n# 403, 404, 410, 500,\n\n\n@view_config(context=HTTPException)\n@notfound_view_config(append_slash=HTTPMovedPermanently)\ndef httpexception_view(exc, request):\n    # This special case exists for the easter egg that appears on the 404\n    # response page. We don't generally allow youtube embeds, but we make an\n    # except for this one.\n    if isinstance(exc, HTTPNotFound):\n        request.find_service(name=\"csp\").merge({\n            \"frame-src\": [\"https://www.youtube-nocookie.com\"],\n            \"script-src\": [\"https://www.youtube.com\", \"https://s.ytimg.com\"],\n        })\n    try:\n        # Lightweight version of 404 page for `/simple/`\n        if (isinstance(exc, HTTPNotFound) and\n                request.path.startswith(\"/simple/\")):\n            response = Response(\n                body=\"404 Not Found\",\n                content_type=\"text/plain\"\n            )\n        else:\n            response = render_to_response(\n                \"{}.html\".format(exc.status_code),\n                {},\n                request=request,\n            )\n    except LookupError:\n        # We don't have a customized template for this error, so we'll just let\n        # the default happen instead.\n        return exc\n\n    # Copy over the important values from our HTTPException to our new response\n    # object.\n    response.status = exc.status\n    response.headers.extend(\n        (k, v) for k, v in exc.headers.items()\n        if k not in response.headers\n    )\n\n    return response\n\n\n@forbidden_view_config()\n@exception_view_config(PredicateMismatch)\ndef forbidden(exc, request, redirect_to=\"accounts.login\"):\n    # If the forbidden error is because the user isn't logged in, then we'll\n    # redirect them to the log in page.\n    if request.authenticated_userid is None:\n        url = request.route_url(\n            redirect_to,\n            _query={REDIRECT_FIELD_NAME: request.path_qs},\n        )\n        return HTTPSeeOther(url)\n\n    # If we've reached here, then the user is logged in and they are genuinely\n    # not allowed to access this page.\n    return httpexception_view(exc, request)\n\n\n@view_config(\n    route_name=\"robots.txt\",\n    renderer=\"robots.txt\",\n    decorator=[\n        cache_control(1 * 24 * 60 * 60),         # 1 day\n        origin_cache(\n            1 * 24 * 60 * 60,                    # 1 day\n            stale_while_revalidate=6 * 60 * 60,  # 6 hours\n            stale_if_error=1 * 24 * 60 * 60,     # 1 day\n        ),\n    ],\n)\ndef robotstxt(request):\n    request.response.content_type = \"text/plain\"\n    return {}\n\n\n@view_config(\n    route_name=\"opensearch.xml\",\n    renderer=\"opensearch.xml\",\n    decorator=[\n        cache_control(1 * 24 * 60 * 60),         # 1 day\n        origin_cache(\n            1 * 24 * 60 * 60,                    # 1 day\n            stale_while_revalidate=6 * 60 * 60,  # 6 hours\n            stale_if_error=1 * 24 * 60 * 60,     # 1 day\n        )\n    ]\n)\ndef opensearchxml(request):\n    request.response.content_type = \"text/xml\"\n    return {}\n\n\n@view_config(\n    route_name=\"index\",\n    renderer=\"index.html\",\n    decorator=[\n        origin_cache(\n            1 * 60 * 60,                      # 1 hour\n            stale_while_revalidate=10 * 60,   # 10 minutes\n            stale_if_error=1 * 24 * 60 * 60,  # 1 day\n            keys=[\"all-projects\", \"trending\"],\n        ),\n    ]\n)\ndef index(request):\n    project_names = [\n        r[0] for r in (\n            request.db.query(Project.name)\n                   .order_by(Project.zscore.desc().nullslast(),\n                             func.random())\n                   .limit(5)\n                   .all())\n    ]\n    release_a = aliased(\n        Release,\n        request.db.query(Release)\n                  .distinct(Release.name)\n                  .filter(Release.name.in_(project_names))\n                  .order_by(Release.name,\n                            Release.is_prerelease.nullslast(),\n                            Release._pypi_ordering.desc())\n                  .subquery(),\n    )\n    trending_projects = (\n        request.db.query(release_a)\n               .options(joinedload(release_a.project))\n               .order_by(func.array_idx(project_names, release_a.name))\n               .all()\n    )\n\n    latest_releases = (\n        request.db.query(Release)\n                  .options(joinedload(Release.project))\n                  .order_by(Release.created.desc())\n                  .limit(5)\n                  .all()\n    )\n\n    counts = dict(\n        request.db.query(RowCount.table_name, RowCount.count)\n                  .filter(\n                      RowCount.table_name.in_([\n                          Project.__tablename__,\n                          Release.__tablename__,\n                          File.__tablename__,\n                          User.__tablename__,\n                      ]))\n                  .all()\n    )\n\n    return {\n        \"latest_releases\": latest_releases,\n        \"trending_projects\": trending_projects,\n        \"num_projects\": counts.get(Project.__tablename__, 0),\n        \"num_releases\": counts.get(Release.__tablename__, 0),\n        \"num_files\": counts.get(File.__tablename__, 0),\n        \"num_users\": counts.get(User.__tablename__, 0),\n    }\n\n\n@view_config(\n    route_name=\"search\",\n    renderer=\"search/results.html\",\n    decorator=[\n        origin_cache(\n            1 * 60 * 60,                      # 1 hour\n            stale_while_revalidate=10 * 60,   # 10 minutes\n            stale_if_error=1 * 24 * 60 * 60,  # 1 day\n            keys=[\"all-projects\"],\n        )\n    ],\n)\ndef search(request):\n\n    q = request.params.get(\"q\", '')\n\n    if q:\n        should = []\n        for field in SEARCH_FIELDS:\n            kw = {\"query\": q}\n            if field in SEARCH_BOOSTS:\n                kw[\"boost\"] = SEARCH_BOOSTS[field]\n            should.append(Q(\"match\", **{field: kw}))\n\n        # Add a prefix query if ``q`` is longer than one character.\n        if len(q) > 1:\n            should.append(Q('prefix', normalized_name=q))\n\n        query = request.es.query(\"dis_max\", queries=should)\n        query = query.suggest(\"name_suggestion\", q, term={\"field\": \"name\"})\n    else:\n        query = request.es.query()\n\n    if request.params.get(\"o\"):\n        sort_key = request.params[\"o\"]\n        if sort_key.startswith(\"-\"):\n            sort = {\n                sort_key[1:]: {\n                    \"order\": \"desc\",\n                    \"unmapped_type\": \"long\",\n                },\n            }\n        else:\n            sort = {\n                sort_key: {\n                    \"unmapped_type\": \"long\",\n                }\n            }\n\n        query = query.sort(sort)\n\n    # Require match to all specified classifiers\n    for classifier in request.params.getall(\"c\"):\n        query = query.filter(\"terms\", classifiers=[classifier])\n\n    try:\n        page_num = int(request.params.get(\"page\", 1))\n    except ValueError:\n        raise HTTPBadRequest(\"'page' must be an integer.\")\n\n    page = ElasticsearchPage(\n        query,\n        page=page_num,\n        url_maker=paginate_url_factory(request),\n    )\n\n    if page.page_count and page_num > page.page_count:\n        return HTTPNotFound()\n\n    available_filters = collections.defaultdict(list)\n\n    classifiers_q = (\n        request.db.query(Classifier)\n        .with_entities(Classifier.classifier)\n        .filter(\n            exists([release_classifiers.c.trove_id])\n            .where(release_classifiers.c.trove_id == Classifier.id)\n        )\n        .order_by(Classifier.classifier)\n    )\n\n    for cls in classifiers_q:\n        first, *_ = cls.classifier.split(' :: ')\n        available_filters[first].append(cls.classifier)\n\n    def filter_key(item):\n        try:\n            return 0, SEARCH_FILTER_ORDER.index(item[0]), item[0]\n        except ValueError:\n            return 1, 0, item[0]\n\n    return {\n        \"page\": page,\n        \"term\": q,\n        \"order\": request.params.get(\"o\", ''),\n        \"available_filters\": sorted(available_filters.items(), key=filter_key),\n        \"applied_filters\": request.params.getall(\"c\"),\n    }\n\n\n@view_config(\n    route_name=\"includes.current-user-indicator\",\n    renderer=\"includes/current-user-indicator.html\",\n    uses_session=True,\n)\ndef current_user_indicator(request):\n    return {}\n\n\n@view_config(\n    route_name=\"includes.flash-messages\",\n    renderer=\"includes/flash-messages.html\",\n    uses_session=True,\n)\ndef flash_messages(request):\n    return {}\n\n\n@view_config(route_name=\"health\", renderer=\"string\")\ndef health(request):\n    # This will ensure that we can access the database and run queries against\n    # it without doing anything that will take a lock or block other queries.\n    request.db.execute(\"SELECT 1\")\n\n    # Nothing will actually check this, but it's a little nicer to have\n    # something to return besides an empty body.\n    return \"OK\"\n\n\n@view_config(route_name=\"force-status\")\ndef force_status(request):\n    try:\n        raise exception_response(int(request.matchdict[\"status\"]))\n    except KeyError:\n        raise exception_response(404) from None\n", "path": "warehouse/views.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 collections\n\nfrom pyramid.httpexceptions import (\n    HTTPException, HTTPSeeOther, HTTPMovedPermanently, HTTPNotFound,\n    HTTPBadRequest, exception_response,\n)\nfrom pyramid.exceptions import PredicateMismatch\nfrom pyramid.renderers import render_to_response\nfrom pyramid.response import Response\nfrom pyramid.view import (\n    notfound_view_config, forbidden_view_config, exception_view_config,\n    view_config,\n)\nfrom elasticsearch_dsl import Q\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import aliased, joinedload\nfrom sqlalchemy.sql import exists\n\nfrom warehouse.accounts import REDIRECT_FIELD_NAME\nfrom warehouse.accounts.models import User\nfrom warehouse.cache.origin import origin_cache\nfrom warehouse.cache.http import cache_control\nfrom warehouse.classifiers.models import Classifier\nfrom warehouse.packaging.models import (\n    Project, Release, File, release_classifiers,\n)\nfrom warehouse.utils.row_counter import RowCount\nfrom warehouse.utils.paginate import ElasticsearchPage, paginate_url_factory\n\n\nSEARCH_FIELDS = [\n    \"author\", \"author_email\", \"description\", \"download_url\", \"home_page\",\n    \"keywords\", \"license\", \"maintainer\", \"maintainer_email\", \"normalized_name\",\n    \"platform\", \"summary\",\n]\nSEARCH_BOOSTS = {\n    \"normalized_name\": 10,\n    \"description\": 5,\n    \"keywords\": 5,\n    \"summary\": 5,\n}\nSEARCH_FILTER_ORDER = (\n    \"Programming Language\",\n    \"License\",\n    \"Framework\",\n    \"Topic\",\n    \"Intended Audience\",\n    \"Environment\",\n    \"Operating System\",\n    \"Natural Language\",\n    \"Development Status\",\n)\n\n\n# 403, 404, 410, 500,\n\n\n@view_config(context=HTTPException)\n@notfound_view_config(append_slash=HTTPMovedPermanently)\ndef httpexception_view(exc, request):\n    # This special case exists for the easter egg that appears on the 404\n    # response page. We don't generally allow youtube embeds, but we make an\n    # except for this one.\n    if isinstance(exc, HTTPNotFound):\n        request.find_service(name=\"csp\").merge({\n            \"frame-src\": [\"https://www.youtube-nocookie.com\"],\n            \"script-src\": [\"https://www.youtube.com\", \"https://s.ytimg.com\"],\n        })\n    try:\n        # Lightweight version of 404 page for `/simple/`\n        if (isinstance(exc, HTTPNotFound) and\n                request.path.startswith(\"/simple/\")):\n            response = Response(\n                body=\"404 Not Found\",\n                content_type=\"text/plain\"\n            )\n        else:\n            response = render_to_response(\n                \"{}.html\".format(exc.status_code),\n                {},\n                request=request,\n            )\n    except LookupError:\n        # We don't have a customized template for this error, so we'll just let\n        # the default happen instead.\n        return exc\n\n    # Copy over the important values from our HTTPException to our new response\n    # object.\n    response.status = exc.status\n    response.headers.extend(\n        (k, v) for k, v in exc.headers.items()\n        if k not in response.headers\n    )\n\n    return response\n\n\n@forbidden_view_config()\n@exception_view_config(PredicateMismatch)\ndef forbidden(exc, request, redirect_to=\"accounts.login\"):\n    # If the forbidden error is because the user isn't logged in, then we'll\n    # redirect them to the log in page.\n    if request.authenticated_userid is None:\n        url = request.route_url(\n            redirect_to,\n            _query={REDIRECT_FIELD_NAME: request.path_qs},\n        )\n        return HTTPSeeOther(url)\n\n    # If we've reached here, then the user is logged in and they are genuinely\n    # not allowed to access this page.\n    return httpexception_view(exc, request)\n\n\n@forbidden_view_config(path_info=r\"^/_includes/\")\n@exception_view_config(PredicateMismatch, path_info=r\"^/_includes/\")\ndef forbidden_include(exc, request):\n    # If the forbidden error is for a client-side-include, just return an empty\n    # response instead of redirecting\n    return Response()\n\n\n@view_config(\n    route_name=\"robots.txt\",\n    renderer=\"robots.txt\",\n    decorator=[\n        cache_control(1 * 24 * 60 * 60),         # 1 day\n        origin_cache(\n            1 * 24 * 60 * 60,                    # 1 day\n            stale_while_revalidate=6 * 60 * 60,  # 6 hours\n            stale_if_error=1 * 24 * 60 * 60,     # 1 day\n        ),\n    ],\n)\ndef robotstxt(request):\n    request.response.content_type = \"text/plain\"\n    return {}\n\n\n@view_config(\n    route_name=\"opensearch.xml\",\n    renderer=\"opensearch.xml\",\n    decorator=[\n        cache_control(1 * 24 * 60 * 60),         # 1 day\n        origin_cache(\n            1 * 24 * 60 * 60,                    # 1 day\n            stale_while_revalidate=6 * 60 * 60,  # 6 hours\n            stale_if_error=1 * 24 * 60 * 60,     # 1 day\n        )\n    ]\n)\ndef opensearchxml(request):\n    request.response.content_type = \"text/xml\"\n    return {}\n\n\n@view_config(\n    route_name=\"index\",\n    renderer=\"index.html\",\n    decorator=[\n        origin_cache(\n            1 * 60 * 60,                      # 1 hour\n            stale_while_revalidate=10 * 60,   # 10 minutes\n            stale_if_error=1 * 24 * 60 * 60,  # 1 day\n            keys=[\"all-projects\", \"trending\"],\n        ),\n    ]\n)\ndef index(request):\n    project_names = [\n        r[0] for r in (\n            request.db.query(Project.name)\n                   .order_by(Project.zscore.desc().nullslast(),\n                             func.random())\n                   .limit(5)\n                   .all())\n    ]\n    release_a = aliased(\n        Release,\n        request.db.query(Release)\n                  .distinct(Release.name)\n                  .filter(Release.name.in_(project_names))\n                  .order_by(Release.name,\n                            Release.is_prerelease.nullslast(),\n                            Release._pypi_ordering.desc())\n                  .subquery(),\n    )\n    trending_projects = (\n        request.db.query(release_a)\n               .options(joinedload(release_a.project))\n               .order_by(func.array_idx(project_names, release_a.name))\n               .all()\n    )\n\n    latest_releases = (\n        request.db.query(Release)\n                  .options(joinedload(Release.project))\n                  .order_by(Release.created.desc())\n                  .limit(5)\n                  .all()\n    )\n\n    counts = dict(\n        request.db.query(RowCount.table_name, RowCount.count)\n                  .filter(\n                      RowCount.table_name.in_([\n                          Project.__tablename__,\n                          Release.__tablename__,\n                          File.__tablename__,\n                          User.__tablename__,\n                      ]))\n                  .all()\n    )\n\n    return {\n        \"latest_releases\": latest_releases,\n        \"trending_projects\": trending_projects,\n        \"num_projects\": counts.get(Project.__tablename__, 0),\n        \"num_releases\": counts.get(Release.__tablename__, 0),\n        \"num_files\": counts.get(File.__tablename__, 0),\n        \"num_users\": counts.get(User.__tablename__, 0),\n    }\n\n\n@view_config(\n    route_name=\"search\",\n    renderer=\"search/results.html\",\n    decorator=[\n        origin_cache(\n            1 * 60 * 60,                      # 1 hour\n            stale_while_revalidate=10 * 60,   # 10 minutes\n            stale_if_error=1 * 24 * 60 * 60,  # 1 day\n            keys=[\"all-projects\"],\n        )\n    ],\n)\ndef search(request):\n\n    q = request.params.get(\"q\", '')\n\n    if q:\n        should = []\n        for field in SEARCH_FIELDS:\n            kw = {\"query\": q}\n            if field in SEARCH_BOOSTS:\n                kw[\"boost\"] = SEARCH_BOOSTS[field]\n            should.append(Q(\"match\", **{field: kw}))\n\n        # Add a prefix query if ``q`` is longer than one character.\n        if len(q) > 1:\n            should.append(Q('prefix', normalized_name=q))\n\n        query = request.es.query(\"dis_max\", queries=should)\n        query = query.suggest(\"name_suggestion\", q, term={\"field\": \"name\"})\n    else:\n        query = request.es.query()\n\n    if request.params.get(\"o\"):\n        sort_key = request.params[\"o\"]\n        if sort_key.startswith(\"-\"):\n            sort = {\n                sort_key[1:]: {\n                    \"order\": \"desc\",\n                    \"unmapped_type\": \"long\",\n                },\n            }\n        else:\n            sort = {\n                sort_key: {\n                    \"unmapped_type\": \"long\",\n                }\n            }\n\n        query = query.sort(sort)\n\n    # Require match to all specified classifiers\n    for classifier in request.params.getall(\"c\"):\n        query = query.filter(\"terms\", classifiers=[classifier])\n\n    try:\n        page_num = int(request.params.get(\"page\", 1))\n    except ValueError:\n        raise HTTPBadRequest(\"'page' must be an integer.\")\n\n    page = ElasticsearchPage(\n        query,\n        page=page_num,\n        url_maker=paginate_url_factory(request),\n    )\n\n    if page.page_count and page_num > page.page_count:\n        return HTTPNotFound()\n\n    available_filters = collections.defaultdict(list)\n\n    classifiers_q = (\n        request.db.query(Classifier)\n        .with_entities(Classifier.classifier)\n        .filter(\n            exists([release_classifiers.c.trove_id])\n            .where(release_classifiers.c.trove_id == Classifier.id)\n        )\n        .order_by(Classifier.classifier)\n    )\n\n    for cls in classifiers_q:\n        first, *_ = cls.classifier.split(' :: ')\n        available_filters[first].append(cls.classifier)\n\n    def filter_key(item):\n        try:\n            return 0, SEARCH_FILTER_ORDER.index(item[0]), item[0]\n        except ValueError:\n            return 1, 0, item[0]\n\n    return {\n        \"page\": page,\n        \"term\": q,\n        \"order\": request.params.get(\"o\", ''),\n        \"available_filters\": sorted(available_filters.items(), key=filter_key),\n        \"applied_filters\": request.params.getall(\"c\"),\n    }\n\n\n@view_config(\n    route_name=\"includes.current-user-indicator\",\n    renderer=\"includes/current-user-indicator.html\",\n    uses_session=True,\n)\ndef current_user_indicator(request):\n    return {}\n\n\n@view_config(\n    route_name=\"includes.flash-messages\",\n    renderer=\"includes/flash-messages.html\",\n    uses_session=True,\n)\ndef flash_messages(request):\n    return {}\n\n\n@view_config(route_name=\"health\", renderer=\"string\")\ndef health(request):\n    # This will ensure that we can access the database and run queries against\n    # it without doing anything that will take a lock or block other queries.\n    request.db.execute(\"SELECT 1\")\n\n    # Nothing will actually check this, but it's a little nicer to have\n    # something to return besides an empty body.\n    return \"OK\"\n\n\n@view_config(route_name=\"force-status\")\ndef force_status(request):\n    try:\n        raise exception_response(int(request.matchdict[\"status\"]))\n    except KeyError:\n        raise exception_response(404) from None\n", "path": "warehouse/views.py"}]}