We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Version's page is slow
Currently, we list all versions in one page. This is a problem for projects that have thousands of versions (it's difficult to search the versions and very slow for the db)

https://readthedocs.org/projects/docs/versions/

One idea is to don't list deactivated versions and use a form instead. Not really sure how that looks like, we still need a way to list all versions. If we put an auto complete widget, does the user know what version wants to activate?

Ref https://github.com/readthedocs/readthedocs.org/issues/6068#issuecomment-520908761

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `readthedocs/projects/views/public.py`
Content:
```
1 """Public project views."""
2 
3 import json
4 import logging
5 import mimetypes
6 import operator
7 import os
8 from collections import OrderedDict
9 
10 import requests
11 from django.conf import settings
12 from django.contrib import messages
13 from django.contrib.auth.models import User
14 from django.core.cache import cache
15 from django.core.files.storage import get_storage_class
16 from django.db.models import prefetch_related_objects
17 from django.http import HttpResponse, HttpResponseRedirect
18 from django.shortcuts import get_object_or_404, render, redirect
19 from django.urls import reverse
20 from django.views.decorators.cache import never_cache
21 from django.views.generic import DetailView, ListView
22 from taggit.models import Tag
23 
24 from readthedocs.analytics.tasks import analytics_event
25 from readthedocs.analytics.utils import get_client_ip
26 from readthedocs.builds.constants import LATEST
27 from readthedocs.builds.models import Version
28 from readthedocs.builds.views import BuildTriggerMixin
29 from readthedocs.projects.models import Project
30 from readthedocs.projects.templatetags.projects_tags import sort_version_aware
31 
32 from .base import ProjectOnboardMixin
33 
34 
35 log = logging.getLogger(__name__)
36 search_log = logging.getLogger(__name__ + '.search')
37 mimetypes.add_type('application/epub+zip', '.epub')
38 
39 
40 class ProjectIndex(ListView):
41 
42     """List view of public :py:class:`Project` instances."""
43 
44     model = Project
45 
46     def get_queryset(self):
47         queryset = Project.objects.public(self.request.user)
48         queryset = queryset.exclude(users__profile__banned=True)
49 
50         if self.kwargs.get('tag'):
51             self.tag = get_object_or_404(Tag, slug=self.kwargs.get('tag'))
52             queryset = queryset.filter(tags__slug__in=[self.tag.slug])
53         else:
54             self.tag = None
55 
56         if self.kwargs.get('username'):
57             self.user = get_object_or_404(
58                 User,
59                 username=self.kwargs.get('username'),
60             )
61             queryset = queryset.filter(user=self.user)
62         else:
63             self.user = None
64 
65         return queryset
66 
67     def get_context_data(self, **kwargs):
68         context = super().get_context_data(**kwargs)
69         context['person'] = self.user
70         context['tag'] = self.tag
71         return context
72 
73 
74 def project_redirect(request, invalid_project_slug):
75     """
76     Redirect project slugs that have underscores (``_``).
77 
78     Slugs with underscores are no longer allowed.
79     Underscores are replaced by ``-`` and then redirected to that URL.
80     """
81     new_project_slug = invalid_project_slug.replace('_', '-')
82     new_path = request.path.replace(invalid_project_slug, new_project_slug)
83     return redirect('{}?{}'.format(
84         new_path,
85         request.GET.urlencode(),
86     ))
87 
88 
89 class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):
90 
91     """Display project onboard steps."""
92 
93     model = Project
94     slug_url_kwarg = 'project_slug'
95 
96     def get_queryset(self):
97         return Project.objects.protected(self.request.user)
98 
99     def get_context_data(self, **kwargs):
100         context = super().get_context_data(**kwargs)
101 
102         project = self.get_object()
103         context['versions'] = self._get_versions(project)
104 
105         protocol = 'http'
106         if self.request.is_secure():
107             protocol = 'https'
108 
109         version_slug = project.get_default_version()
110 
111         context['badge_url'] = '{}://{}{}?version={}'.format(
112             protocol,
113             settings.PRODUCTION_DOMAIN,
114             reverse('project_badge', args=[project.slug]),
115             project.get_default_version(),
116         )
117         context['site_url'] = '{url}?badge={version}'.format(
118             url=project.get_docs_url(version_slug),
119             version=version_slug,
120         )
121 
122         return context
123 
124 
125 @never_cache
126 def project_badge(request, project_slug):
127     """Return a sweet badge for the project."""
128     style = request.GET.get('style', 'flat')
129     if style not in (
130             'flat',
131             'plastic',
132             'flat-square',
133             'for-the-badge',
134             'social',
135     ):
136         style = 'flat'
137 
138     # Get the local path to the badge files
139     badge_path = os.path.join(
140         os.path.dirname(__file__),
141         '..',
142         'static',
143         'projects',
144         'badges',
145         '%s-' + style + '.svg',
146     )
147 
148     version_slug = request.GET.get('version', LATEST)
149     file_path = badge_path % 'unknown'
150 
151     version = Version.objects.public(request.user).filter(
152         project__slug=project_slug,
153         slug=version_slug,
154     ).first()
155 
156     if version:
157         last_build = version.builds.filter(
158             type='html',
159             state='finished',
160         ).order_by('-date').first()
161         if last_build:
162             if last_build.success:
163                 file_path = badge_path % 'passing'
164             else:
165                 file_path = badge_path % 'failing'
166 
167     try:
168         with open(file_path) as fd:
169             return HttpResponse(
170                 fd.read(),
171                 content_type='image/svg+xml',
172             )
173     except (IOError, OSError):
174         log.exception(
175             'Failed to read local filesystem while serving a docs badge',
176         )
177         return HttpResponse(status=503)
178 
179 
180 def project_downloads(request, project_slug):
181     """A detail view for a project with various downloads."""
182     project = get_object_or_404(
183         Project.objects.protected(request.user),
184         slug=project_slug,
185     )
186     versions = Version.internal.public(user=request.user, project=project)
187     versions = sort_version_aware(versions)
188     version_data = OrderedDict()
189     for version in versions:
190         data = version.get_downloads()
191         # Don't show ones that have no downloads.
192         if data:
193             version_data[version] = data
194 
195     return render(
196         request,
197         'projects/project_downloads.html',
198         {
199             'project': project,
200             'version_data': version_data,
201             'versions': versions,
202         },
203     )
204 
205 
206 def project_download_media(request, project_slug, type_, version_slug):
207     """
208     Download a specific piece of media.
209 
210     Perform an auth check if serving in private mode.
211 
212     .. warning:: This is linked directly from the HTML pages.
213                  It should only care about the Version permissions,
214                  not the actual Project permissions.
215     """
216     version = get_object_or_404(
217         Version.objects.public(user=request.user),
218         project__slug=project_slug,
219         slug=version_slug,
220     )
221 
222     # Send media download to analytics - sensitive data is anonymized
223     analytics_event.delay(
224         event_category='Build Media',
225         event_action=f'Download {type_}',
226         event_label=str(version),
227         ua=request.META.get('HTTP_USER_AGENT'),
228         uip=get_client_ip(request),
229     )
230 
231     if settings.DEFAULT_PRIVACY_LEVEL == 'public' or settings.DEBUG:
232 
233         storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
234         storage_path = version.project.get_storage_path(
235             type_=type_, version_slug=version_slug,
236             version_type=version.type,
237         )
238         if storage.exists(storage_path):
239             return HttpResponseRedirect(storage.url(storage_path))
240 
241         media_path = os.path.join(
242             settings.MEDIA_URL,
243             type_,
244             project_slug,
245             version_slug,
246             '%s.%s' % (project_slug, type_.replace('htmlzip', 'zip')),
247         )
248         return HttpResponseRedirect(media_path)
249 
250     # Get relative media path
251     path = (
252         version.project.get_production_media_path(
253             type_=type_,
254             version_slug=version_slug,
255         ).replace(settings.PRODUCTION_ROOT, '/prod_artifacts')
256     )
257     content_type, encoding = mimetypes.guess_type(path)
258     content_type = content_type or 'application/octet-stream'
259     response = HttpResponse(content_type=content_type)
260     if encoding:
261         response['Content-Encoding'] = encoding
262     response['X-Accel-Redirect'] = path
263     # Include version in filename; this fixes a long-standing bug
264     filename = '{}-{}.{}'.format(
265         project_slug,
266         version_slug,
267         path.split('.')[-1],
268     )
269     response['Content-Disposition'] = 'filename=%s' % filename
270     return response
271 
272 
273 def project_versions(request, project_slug):
274     """
275     Project version list view.
276 
277     Shows the available versions and lets the user choose which ones to build.
278     """
279     project = get_object_or_404(
280         Project.objects.protected(request.user),
281         slug=project_slug,
282     )
283 
284     versions = Version.internal.public(
285         user=request.user,
286         project=project,
287         only_active=False,
288     )
289     active_versions = versions.filter(active=True)
290     inactive_versions = versions.filter(active=False)
291 
292     # If there's a wiped query string, check the string against the versions
293     # list and display a success message. Deleting directories doesn't know how
294     # to fail.  :)
295     wiped = request.GET.get('wipe', '')
296     wiped_version = versions.filter(slug=wiped)
297     if wiped and wiped_version.count():
298         messages.success(request, 'Version wiped: ' + wiped)
299 
300     # Optimize project permission checks
301     prefetch_related_objects([project], 'users')
302 
303     return render(
304         request,
305         'projects/project_version_list.html',
306         {
307             'inactive_versions': inactive_versions,
308             'active_versions': active_versions,
309             'project': project,
310         },
311     )
312 
313 
314 def project_analytics(request, project_slug):
315     """Have a analytics API placeholder."""
316     project = get_object_or_404(
317         Project.objects.protected(request.user),
318         slug=project_slug,
319     )
320     analytics_cache = cache.get('analytics:%s' % project_slug)
321     if analytics_cache:
322         analytics = json.loads(analytics_cache)
323     else:
324         try:
325             resp = requests.get(
326                 '{host}/api/v1/index/1/heatmap/'.format(
327                     host=settings.GROK_API_HOST,
328                 ),
329                 params={'project': project.slug, 'days': 7, 'compare': True},
330             )
331             analytics = resp.json()
332             cache.set('analytics:%s' % project_slug, resp.content, 1800)
333         except requests.exceptions.RequestException:
334             analytics = None
335 
336     if analytics:
337         page_list = list(
338             reversed(
339                 sorted(
340                     list(analytics['page'].items()),
341                     key=operator.itemgetter(1),
342                 ),
343             ),
344         )
345         version_list = list(
346             reversed(
347                 sorted(
348                     list(analytics['version'].items()),
349                     key=operator.itemgetter(1),
350                 ),
351             ),
352         )
353     else:
354         page_list = []
355         version_list = []
356 
357     full = request.GET.get('full')
358     if not full:
359         page_list = page_list[:20]
360         version_list = version_list[:20]
361 
362     return render(
363         request,
364         'projects/project_analytics.html',
365         {
366             'project': project,
367             'analytics': analytics,
368             'page_list': page_list,
369             'version_list': version_list,
370             'full': full,
371         },
372     )
373 
374 
375 def project_embed(request, project_slug):
376     """Have a content API placeholder."""
377     project = get_object_or_404(
378         Project.objects.protected(request.user),
379         slug=project_slug,
380     )
381     version = project.versions.get(slug=LATEST)
382     files = version.imported_files.filter(
383         name__endswith='.html',
384     ).order_by('path')
385 
386     return render(
387         request,
388         'projects/project_embed.html',
389         {
390             'project': project,
391             'files': files,
392             'settings': {
393                 'PUBLIC_API_URL': settings.PUBLIC_API_URL,
394                 'URI': request.build_absolute_uri(location='/').rstrip('/'),
395             },
396         },
397     )
398
```
--- 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/readthedocs/projects/views/public.py b/readthedocs/projects/views/public.py
--- a/readthedocs/projects/views/public.py
+++ b/readthedocs/projects/views/public.py
@@ -287,7 +287,14 @@
         only_active=False,
     )
     active_versions = versions.filter(active=True)
+
+    # Limit inactive versions in case a project has a large number of branches or tags
+    # Filter inactive versions based on the query string
     inactive_versions = versions.filter(active=False)
+    version_filter = request.GET.get('version_filter', '')
+    if version_filter:
+        inactive_versions = inactive_versions.filter(verbose_name__icontains=version_filter)
+    inactive_versions = inactive_versions[:100]
 
     # If there's a wiped query string, check the string against the versions
     # list and display a success message. Deleting directories doesn't know how

{"golden_diff": "diff --git a/readthedocs/projects/views/public.py b/readthedocs/projects/views/public.py\n--- a/readthedocs/projects/views/public.py\n+++ b/readthedocs/projects/views/public.py\n@@ -287,7 +287,14 @@\n         only_active=False,\n     )\n     active_versions = versions.filter(active=True)\n+\n+    # Limit inactive versions in case a project has a large number of branches or tags\n+    # Filter inactive versions based on the query string\n     inactive_versions = versions.filter(active=False)\n+    version_filter = request.GET.get('version_filter', '')\n+    if version_filter:\n+        inactive_versions = inactive_versions.filter(verbose_name__icontains=version_filter)\n+    inactive_versions = inactive_versions[:100]\n \n     # If there's a wiped query string, check the string against the versions\n     # list and display a success message. Deleting directories doesn't know how\n", "issue": "Version's page is slow\nCurrently, we list all versions in one page. This is a problem for projects that have thousands of versions (it's difficult to search the versions and very slow for the db)\r\n\r\nhttps://readthedocs.org/projects/docs/versions/\r\n\r\nOne idea is to don't list deactivated versions and use a form instead. Not really sure how that looks like, we still need a way to list all versions. If we put an auto complete widget, does the user know what version wants to activate?\r\n\r\nRef https://github.com/readthedocs/readthedocs.org/issues/6068#issuecomment-520908761\n", "before_files": [{"content": "\"\"\"Public project views.\"\"\"\n\nimport json\nimport logging\nimport mimetypes\nimport operator\nimport os\nfrom collections import OrderedDict\n\nimport requests\nfrom django.conf import settings\nfrom django.contrib import messages\nfrom django.contrib.auth.models import User\nfrom django.core.cache import cache\nfrom django.core.files.storage import get_storage_class\nfrom django.db.models import prefetch_related_objects\nfrom django.http import HttpResponse, HttpResponseRedirect\nfrom django.shortcuts import get_object_or_404, render, redirect\nfrom django.urls import reverse\nfrom django.views.decorators.cache import never_cache\nfrom django.views.generic import DetailView, ListView\nfrom taggit.models import Tag\n\nfrom readthedocs.analytics.tasks import analytics_event\nfrom readthedocs.analytics.utils import get_client_ip\nfrom readthedocs.builds.constants import LATEST\nfrom readthedocs.builds.models import Version\nfrom readthedocs.builds.views import BuildTriggerMixin\nfrom readthedocs.projects.models import Project\nfrom readthedocs.projects.templatetags.projects_tags import sort_version_aware\n\nfrom .base import ProjectOnboardMixin\n\n\nlog = logging.getLogger(__name__)\nsearch_log = logging.getLogger(__name__ + '.search')\nmimetypes.add_type('application/epub+zip', '.epub')\n\n\nclass ProjectIndex(ListView):\n\n    \"\"\"List view of public :py:class:`Project` instances.\"\"\"\n\n    model = Project\n\n    def get_queryset(self):\n        queryset = Project.objects.public(self.request.user)\n        queryset = queryset.exclude(users__profile__banned=True)\n\n        if self.kwargs.get('tag'):\n            self.tag = get_object_or_404(Tag, slug=self.kwargs.get('tag'))\n            queryset = queryset.filter(tags__slug__in=[self.tag.slug])\n        else:\n            self.tag = None\n\n        if self.kwargs.get('username'):\n            self.user = get_object_or_404(\n                User,\n                username=self.kwargs.get('username'),\n            )\n            queryset = queryset.filter(user=self.user)\n        else:\n            self.user = None\n\n        return queryset\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context['person'] = self.user\n        context['tag'] = self.tag\n        return context\n\n\ndef project_redirect(request, invalid_project_slug):\n    \"\"\"\n    Redirect project slugs that have underscores (``_``).\n\n    Slugs with underscores are no longer allowed.\n    Underscores are replaced by ``-`` and then redirected to that URL.\n    \"\"\"\n    new_project_slug = invalid_project_slug.replace('_', '-')\n    new_path = request.path.replace(invalid_project_slug, new_project_slug)\n    return redirect('{}?{}'.format(\n        new_path,\n        request.GET.urlencode(),\n    ))\n\n\nclass ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):\n\n    \"\"\"Display project onboard steps.\"\"\"\n\n    model = Project\n    slug_url_kwarg = 'project_slug'\n\n    def get_queryset(self):\n        return Project.objects.protected(self.request.user)\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n\n        project = self.get_object()\n        context['versions'] = self._get_versions(project)\n\n        protocol = 'http'\n        if self.request.is_secure():\n            protocol = 'https'\n\n        version_slug = project.get_default_version()\n\n        context['badge_url'] = '{}://{}{}?version={}'.format(\n            protocol,\n            settings.PRODUCTION_DOMAIN,\n            reverse('project_badge', args=[project.slug]),\n            project.get_default_version(),\n        )\n        context['site_url'] = '{url}?badge={version}'.format(\n            url=project.get_docs_url(version_slug),\n            version=version_slug,\n        )\n\n        return context\n\n\n@never_cache\ndef project_badge(request, project_slug):\n    \"\"\"Return a sweet badge for the project.\"\"\"\n    style = request.GET.get('style', 'flat')\n    if style not in (\n            'flat',\n            'plastic',\n            'flat-square',\n            'for-the-badge',\n            'social',\n    ):\n        style = 'flat'\n\n    # Get the local path to the badge files\n    badge_path = os.path.join(\n        os.path.dirname(__file__),\n        '..',\n        'static',\n        'projects',\n        'badges',\n        '%s-' + style + '.svg',\n    )\n\n    version_slug = request.GET.get('version', LATEST)\n    file_path = badge_path % 'unknown'\n\n    version = Version.objects.public(request.user).filter(\n        project__slug=project_slug,\n        slug=version_slug,\n    ).first()\n\n    if version:\n        last_build = version.builds.filter(\n            type='html',\n            state='finished',\n        ).order_by('-date').first()\n        if last_build:\n            if last_build.success:\n                file_path = badge_path % 'passing'\n            else:\n                file_path = badge_path % 'failing'\n\n    try:\n        with open(file_path) as fd:\n            return HttpResponse(\n                fd.read(),\n                content_type='image/svg+xml',\n            )\n    except (IOError, OSError):\n        log.exception(\n            'Failed to read local filesystem while serving a docs badge',\n        )\n        return HttpResponse(status=503)\n\n\ndef project_downloads(request, project_slug):\n    \"\"\"A detail view for a project with various downloads.\"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n    versions = Version.internal.public(user=request.user, project=project)\n    versions = sort_version_aware(versions)\n    version_data = OrderedDict()\n    for version in versions:\n        data = version.get_downloads()\n        # Don't show ones that have no downloads.\n        if data:\n            version_data[version] = data\n\n    return render(\n        request,\n        'projects/project_downloads.html',\n        {\n            'project': project,\n            'version_data': version_data,\n            'versions': versions,\n        },\n    )\n\n\ndef project_download_media(request, project_slug, type_, version_slug):\n    \"\"\"\n    Download a specific piece of media.\n\n    Perform an auth check if serving in private mode.\n\n    .. warning:: This is linked directly from the HTML pages.\n                 It should only care about the Version permissions,\n                 not the actual Project permissions.\n    \"\"\"\n    version = get_object_or_404(\n        Version.objects.public(user=request.user),\n        project__slug=project_slug,\n        slug=version_slug,\n    )\n\n    # Send media download to analytics - sensitive data is anonymized\n    analytics_event.delay(\n        event_category='Build Media',\n        event_action=f'Download {type_}',\n        event_label=str(version),\n        ua=request.META.get('HTTP_USER_AGENT'),\n        uip=get_client_ip(request),\n    )\n\n    if settings.DEFAULT_PRIVACY_LEVEL == 'public' or settings.DEBUG:\n\n        storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()\n        storage_path = version.project.get_storage_path(\n            type_=type_, version_slug=version_slug,\n            version_type=version.type,\n        )\n        if storage.exists(storage_path):\n            return HttpResponseRedirect(storage.url(storage_path))\n\n        media_path = os.path.join(\n            settings.MEDIA_URL,\n            type_,\n            project_slug,\n            version_slug,\n            '%s.%s' % (project_slug, type_.replace('htmlzip', 'zip')),\n        )\n        return HttpResponseRedirect(media_path)\n\n    # Get relative media path\n    path = (\n        version.project.get_production_media_path(\n            type_=type_,\n            version_slug=version_slug,\n        ).replace(settings.PRODUCTION_ROOT, '/prod_artifacts')\n    )\n    content_type, encoding = mimetypes.guess_type(path)\n    content_type = content_type or 'application/octet-stream'\n    response = HttpResponse(content_type=content_type)\n    if encoding:\n        response['Content-Encoding'] = encoding\n    response['X-Accel-Redirect'] = path\n    # Include version in filename; this fixes a long-standing bug\n    filename = '{}-{}.{}'.format(\n        project_slug,\n        version_slug,\n        path.split('.')[-1],\n    )\n    response['Content-Disposition'] = 'filename=%s' % filename\n    return response\n\n\ndef project_versions(request, project_slug):\n    \"\"\"\n    Project version list view.\n\n    Shows the available versions and lets the user choose which ones to build.\n    \"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n\n    versions = Version.internal.public(\n        user=request.user,\n        project=project,\n        only_active=False,\n    )\n    active_versions = versions.filter(active=True)\n    inactive_versions = versions.filter(active=False)\n\n    # If there's a wiped query string, check the string against the versions\n    # list and display a success message. Deleting directories doesn't know how\n    # to fail.  :)\n    wiped = request.GET.get('wipe', '')\n    wiped_version = versions.filter(slug=wiped)\n    if wiped and wiped_version.count():\n        messages.success(request, 'Version wiped: ' + wiped)\n\n    # Optimize project permission checks\n    prefetch_related_objects([project], 'users')\n\n    return render(\n        request,\n        'projects/project_version_list.html',\n        {\n            'inactive_versions': inactive_versions,\n            'active_versions': active_versions,\n            'project': project,\n        },\n    )\n\n\ndef project_analytics(request, project_slug):\n    \"\"\"Have a analytics API placeholder.\"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n    analytics_cache = cache.get('analytics:%s' % project_slug)\n    if analytics_cache:\n        analytics = json.loads(analytics_cache)\n    else:\n        try:\n            resp = requests.get(\n                '{host}/api/v1/index/1/heatmap/'.format(\n                    host=settings.GROK_API_HOST,\n                ),\n                params={'project': project.slug, 'days': 7, 'compare': True},\n            )\n            analytics = resp.json()\n            cache.set('analytics:%s' % project_slug, resp.content, 1800)\n        except requests.exceptions.RequestException:\n            analytics = None\n\n    if analytics:\n        page_list = list(\n            reversed(\n                sorted(\n                    list(analytics['page'].items()),\n                    key=operator.itemgetter(1),\n                ),\n            ),\n        )\n        version_list = list(\n            reversed(\n                sorted(\n                    list(analytics['version'].items()),\n                    key=operator.itemgetter(1),\n                ),\n            ),\n        )\n    else:\n        page_list = []\n        version_list = []\n\n    full = request.GET.get('full')\n    if not full:\n        page_list = page_list[:20]\n        version_list = version_list[:20]\n\n    return render(\n        request,\n        'projects/project_analytics.html',\n        {\n            'project': project,\n            'analytics': analytics,\n            'page_list': page_list,\n            'version_list': version_list,\n            'full': full,\n        },\n    )\n\n\ndef project_embed(request, project_slug):\n    \"\"\"Have a content API placeholder.\"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n    version = project.versions.get(slug=LATEST)\n    files = version.imported_files.filter(\n        name__endswith='.html',\n    ).order_by('path')\n\n    return render(\n        request,\n        'projects/project_embed.html',\n        {\n            'project': project,\n            'files': files,\n            'settings': {\n                'PUBLIC_API_URL': settings.PUBLIC_API_URL,\n                'URI': request.build_absolute_uri(location='/').rstrip('/'),\n            },\n        },\n    )\n", "path": "readthedocs/projects/views/public.py"}], "after_files": [{"content": "\"\"\"Public project views.\"\"\"\n\nimport json\nimport logging\nimport mimetypes\nimport operator\nimport os\nfrom collections import OrderedDict\n\nimport requests\nfrom django.conf import settings\nfrom django.contrib import messages\nfrom django.contrib.auth.models import User\nfrom django.core.cache import cache\nfrom django.core.files.storage import get_storage_class\nfrom django.db.models import prefetch_related_objects\nfrom django.http import HttpResponse, HttpResponseRedirect\nfrom django.shortcuts import get_object_or_404, render, redirect\nfrom django.urls import reverse\nfrom django.views.decorators.cache import never_cache\nfrom django.views.generic import DetailView, ListView\nfrom taggit.models import Tag\n\nfrom readthedocs.analytics.tasks import analytics_event\nfrom readthedocs.analytics.utils import get_client_ip\nfrom readthedocs.builds.constants import LATEST\nfrom readthedocs.builds.models import Version\nfrom readthedocs.builds.views import BuildTriggerMixin\nfrom readthedocs.projects.models import Project\nfrom readthedocs.projects.templatetags.projects_tags import sort_version_aware\n\nfrom .base import ProjectOnboardMixin\n\n\nlog = logging.getLogger(__name__)\nsearch_log = logging.getLogger(__name__ + '.search')\nmimetypes.add_type('application/epub+zip', '.epub')\n\n\nclass ProjectIndex(ListView):\n\n    \"\"\"List view of public :py:class:`Project` instances.\"\"\"\n\n    model = Project\n\n    def get_queryset(self):\n        queryset = Project.objects.public(self.request.user)\n        queryset = queryset.exclude(users__profile__banned=True)\n\n        if self.kwargs.get('tag'):\n            self.tag = get_object_or_404(Tag, slug=self.kwargs.get('tag'))\n            queryset = queryset.filter(tags__slug__in=[self.tag.slug])\n        else:\n            self.tag = None\n\n        if self.kwargs.get('username'):\n            self.user = get_object_or_404(\n                User,\n                username=self.kwargs.get('username'),\n            )\n            queryset = queryset.filter(user=self.user)\n        else:\n            self.user = None\n\n        return queryset\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context['person'] = self.user\n        context['tag'] = self.tag\n        return context\n\n\ndef project_redirect(request, invalid_project_slug):\n    \"\"\"\n    Redirect project slugs that have underscores (``_``).\n\n    Slugs with underscores are no longer allowed.\n    Underscores are replaced by ``-`` and then redirected to that URL.\n    \"\"\"\n    new_project_slug = invalid_project_slug.replace('_', '-')\n    new_path = request.path.replace(invalid_project_slug, new_project_slug)\n    return redirect('{}?{}'.format(\n        new_path,\n        request.GET.urlencode(),\n    ))\n\n\nclass ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):\n\n    \"\"\"Display project onboard steps.\"\"\"\n\n    model = Project\n    slug_url_kwarg = 'project_slug'\n\n    def get_queryset(self):\n        return Project.objects.protected(self.request.user)\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n\n        project = self.get_object()\n        context['versions'] = self._get_versions(project)\n\n        protocol = 'http'\n        if self.request.is_secure():\n            protocol = 'https'\n\n        version_slug = project.get_default_version()\n\n        context['badge_url'] = '{}://{}{}?version={}'.format(\n            protocol,\n            settings.PRODUCTION_DOMAIN,\n            reverse('project_badge', args=[project.slug]),\n            project.get_default_version(),\n        )\n        context['site_url'] = '{url}?badge={version}'.format(\n            url=project.get_docs_url(version_slug),\n            version=version_slug,\n        )\n\n        return context\n\n\n@never_cache\ndef project_badge(request, project_slug):\n    \"\"\"Return a sweet badge for the project.\"\"\"\n    style = request.GET.get('style', 'flat')\n    if style not in (\n            'flat',\n            'plastic',\n            'flat-square',\n            'for-the-badge',\n            'social',\n    ):\n        style = 'flat'\n\n    # Get the local path to the badge files\n    badge_path = os.path.join(\n        os.path.dirname(__file__),\n        '..',\n        'static',\n        'projects',\n        'badges',\n        '%s-' + style + '.svg',\n    )\n\n    version_slug = request.GET.get('version', LATEST)\n    file_path = badge_path % 'unknown'\n\n    version = Version.objects.public(request.user).filter(\n        project__slug=project_slug,\n        slug=version_slug,\n    ).first()\n\n    if version:\n        last_build = version.builds.filter(\n            type='html',\n            state='finished',\n        ).order_by('-date').first()\n        if last_build:\n            if last_build.success:\n                file_path = badge_path % 'passing'\n            else:\n                file_path = badge_path % 'failing'\n\n    try:\n        with open(file_path) as fd:\n            return HttpResponse(\n                fd.read(),\n                content_type='image/svg+xml',\n            )\n    except (IOError, OSError):\n        log.exception(\n            'Failed to read local filesystem while serving a docs badge',\n        )\n        return HttpResponse(status=503)\n\n\ndef project_downloads(request, project_slug):\n    \"\"\"A detail view for a project with various downloads.\"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n    versions = Version.internal.public(user=request.user, project=project)\n    versions = sort_version_aware(versions)\n    version_data = OrderedDict()\n    for version in versions:\n        data = version.get_downloads()\n        # Don't show ones that have no downloads.\n        if data:\n            version_data[version] = data\n\n    return render(\n        request,\n        'projects/project_downloads.html',\n        {\n            'project': project,\n            'version_data': version_data,\n            'versions': versions,\n        },\n    )\n\n\ndef project_download_media(request, project_slug, type_, version_slug):\n    \"\"\"\n    Download a specific piece of media.\n\n    Perform an auth check if serving in private mode.\n\n    .. warning:: This is linked directly from the HTML pages.\n                 It should only care about the Version permissions,\n                 not the actual Project permissions.\n    \"\"\"\n    version = get_object_or_404(\n        Version.objects.public(user=request.user),\n        project__slug=project_slug,\n        slug=version_slug,\n    )\n\n    # Send media download to analytics - sensitive data is anonymized\n    analytics_event.delay(\n        event_category='Build Media',\n        event_action=f'Download {type_}',\n        event_label=str(version),\n        ua=request.META.get('HTTP_USER_AGENT'),\n        uip=get_client_ip(request),\n    )\n\n    if settings.DEFAULT_PRIVACY_LEVEL == 'public' or settings.DEBUG:\n\n        storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()\n        storage_path = version.project.get_storage_path(\n            type_=type_, version_slug=version_slug,\n            version_type=version.type,\n        )\n        if storage.exists(storage_path):\n            return HttpResponseRedirect(storage.url(storage_path))\n\n        media_path = os.path.join(\n            settings.MEDIA_URL,\n            type_,\n            project_slug,\n            version_slug,\n            '%s.%s' % (project_slug, type_.replace('htmlzip', 'zip')),\n        )\n        return HttpResponseRedirect(media_path)\n\n    # Get relative media path\n    path = (\n        version.project.get_production_media_path(\n            type_=type_,\n            version_slug=version_slug,\n        ).replace(settings.PRODUCTION_ROOT, '/prod_artifacts')\n    )\n    content_type, encoding = mimetypes.guess_type(path)\n    content_type = content_type or 'application/octet-stream'\n    response = HttpResponse(content_type=content_type)\n    if encoding:\n        response['Content-Encoding'] = encoding\n    response['X-Accel-Redirect'] = path\n    # Include version in filename; this fixes a long-standing bug\n    filename = '{}-{}.{}'.format(\n        project_slug,\n        version_slug,\n        path.split('.')[-1],\n    )\n    response['Content-Disposition'] = 'filename=%s' % filename\n    return response\n\n\ndef project_versions(request, project_slug):\n    \"\"\"\n    Project version list view.\n\n    Shows the available versions and lets the user choose which ones to build.\n    \"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n\n    versions = Version.internal.public(\n        user=request.user,\n        project=project,\n        only_active=False,\n    )\n    active_versions = versions.filter(active=True)\n\n    # Limit inactive versions in case a project has a large number of branches or tags\n    # Filter inactive versions based on the query string\n    inactive_versions = versions.filter(active=False)\n    version_filter = request.GET.get('version_filter', '')\n    if version_filter:\n        inactive_versions = inactive_versions.filter(verbose_name__icontains=version_filter)\n    inactive_versions = inactive_versions[:100]\n\n    # If there's a wiped query string, check the string against the versions\n    # list and display a success message. Deleting directories doesn't know how\n    # to fail.  :)\n    wiped = request.GET.get('wipe', '')\n    wiped_version = versions.filter(slug=wiped)\n    if wiped and wiped_version.count():\n        messages.success(request, 'Version wiped: ' + wiped)\n\n    # Optimize project permission checks\n    prefetch_related_objects([project], 'users')\n\n    return render(\n        request,\n        'projects/project_version_list.html',\n        {\n            'inactive_versions': inactive_versions,\n            'active_versions': active_versions,\n            'project': project,\n        },\n    )\n\n\ndef project_analytics(request, project_slug):\n    \"\"\"Have a analytics API placeholder.\"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n    analytics_cache = cache.get('analytics:%s' % project_slug)\n    if analytics_cache:\n        analytics = json.loads(analytics_cache)\n    else:\n        try:\n            resp = requests.get(\n                '{host}/api/v1/index/1/heatmap/'.format(\n                    host=settings.GROK_API_HOST,\n                ),\n                params={'project': project.slug, 'days': 7, 'compare': True},\n            )\n            analytics = resp.json()\n            cache.set('analytics:%s' % project_slug, resp.content, 1800)\n        except requests.exceptions.RequestException:\n            analytics = None\n\n    if analytics:\n        page_list = list(\n            reversed(\n                sorted(\n                    list(analytics['page'].items()),\n                    key=operator.itemgetter(1),\n                ),\n            ),\n        )\n        version_list = list(\n            reversed(\n                sorted(\n                    list(analytics['version'].items()),\n                    key=operator.itemgetter(1),\n                ),\n            ),\n        )\n    else:\n        page_list = []\n        version_list = []\n\n    full = request.GET.get('full')\n    if not full:\n        page_list = page_list[:20]\n        version_list = version_list[:20]\n\n    return render(\n        request,\n        'projects/project_analytics.html',\n        {\n            'project': project,\n            'analytics': analytics,\n            'page_list': page_list,\n            'version_list': version_list,\n            'full': full,\n        },\n    )\n\n\ndef project_embed(request, project_slug):\n    \"\"\"Have a content API placeholder.\"\"\"\n    project = get_object_or_404(\n        Project.objects.protected(request.user),\n        slug=project_slug,\n    )\n    version = project.versions.get(slug=LATEST)\n    files = version.imported_files.filter(\n        name__endswith='.html',\n    ).order_by('path')\n\n    return render(\n        request,\n        'projects/project_embed.html',\n        {\n            'project': project,\n            'files': files,\n            'settings': {\n                'PUBLIC_API_URL': settings.PUBLIC_API_URL,\n                'URI': request.build_absolute_uri(location='/').rstrip('/'),\n            },\n        },\n    )\n", "path": "readthedocs/projects/views/public.py"}]}