From 613d97e60601f04edc046a3749a6626aab095b49 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 20 Jun 2018 14:09:15 +0200 Subject: [PATCH 01/31] doc_builder: remove duplication around html_theme --- .../templates/doc_builder/conf.py.tmpl | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl index d6eb89ecaa8..e920ec4c3ee 100644 --- a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl +++ b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl @@ -39,22 +39,15 @@ if os.path.exists('_static'): html_static_path.append('{{ static_path }}') # Add RTD Theme only if they aren't overriding it already -using_rtd_theme = False -if 'html_theme' in globals(): - if html_theme in ['default']: +using_rtd_theme = ( + ( + 'html_theme' in globals() and + html_theme in ['default'] and # Allow people to bail with a hack of having an html_style - if not 'html_style' in globals(): - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_style = None - html_theme_options = {} - if 'html_theme_path' in globals(): - html_theme_path.append(sphinx_rtd_theme.get_html_theme_path()) - else: - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - - using_rtd_theme = True -else: + 'html_style' not in globals() + ) or 'html_theme' not in globals() +) +if using_rtd_theme: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_style = None @@ -63,7 +56,6 @@ else: html_theme_path.append(sphinx_rtd_theme.get_html_theme_path()) else: html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - using_rtd_theme = True if globals().get('websupport2_base_url', False): websupport2_base_url = '{{ api_host }}/websupport' From 96a4b94ff529e6df2323ced9d6a47a6295e451f9 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 20 Jun 2018 14:15:46 +0200 Subject: [PATCH 02/31] doc_builder: make it possible to use a different default theme --- readthedocs/doc_builder/backends/sphinx.py | 2 ++ .../doc_builder/templates/doc_builder/conf.py.tmpl | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/readthedocs/doc_builder/backends/sphinx.py b/readthedocs/doc_builder/backends/sphinx.py index 8a4323fc3ae..2ec28946842 100644 --- a/readthedocs/doc_builder/backends/sphinx.py +++ b/readthedocs/doc_builder/backends/sphinx.py @@ -98,6 +98,8 @@ def get_config_params(self): downloads = api.version(self.version.pk).get()['downloads'] data = { + 'html_theme': 'sphinx_rtd_theme', + 'html_theme_import': 'sphinx_rtd_theme', 'current_version': self.version.verbose_name, 'project': self.project, 'version': self.version, diff --git a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl index e920ec4c3ee..b138a568b5a 100644 --- a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl +++ b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl @@ -14,6 +14,7 @@ # +import importlib import sys import os.path from six import string_types @@ -48,14 +49,14 @@ using_rtd_theme = ( ) or 'html_theme' not in globals() ) if using_rtd_theme: - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' + theme = importlib.import_module('{{ html_theme_import }}') + html_theme = '{{ html_theme }}' html_style = None html_theme_options = {} if 'html_theme_path' in globals(): - html_theme_path.append(sphinx_rtd_theme.get_html_theme_path()) + html_theme_path.append(theme.get_html_theme_path()) else: - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + html_theme_path = [theme.get_html_theme_path()] if globals().get('websupport2_base_url', False): websupport2_base_url = '{{ api_host }}/websupport' From 46db5d9e81795bbfc4e2de7d9f0f7a78859b5ac3 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Mon, 20 Aug 2018 12:09:31 -0700 Subject: [PATCH 03/31] Enable timezone support and set timezone to UTC --- readthedocs/core/fixtures/eric.json | 12 +- .../core/management/commands/clean_builds.py | 5 +- readthedocs/oauth/services/base.py | 7 +- readthedocs/projects/fixtures/test_auth.json | 8 +- readthedocs/projects/fixtures/test_data.json | 112 +++++++++--------- readthedocs/projects/tasks.py | 5 +- readthedocs/projects/views/base.py | 5 +- readthedocs/rtd_tests/tests/test_project.py | 5 +- .../rtd_tests/tests/test_project_views.py | 8 +- readthedocs/search/indexes.py | 5 +- readthedocs/settings/base.py | 3 +- 11 files changed, 93 insertions(+), 82 deletions(-) diff --git a/readthedocs/core/fixtures/eric.json b/readthedocs/core/fixtures/eric.json index 1ab201c909f..9f5d03f1686 100644 --- a/readthedocs/core/fixtures/eric.json +++ b/readthedocs/core/fixtures/eric.json @@ -9,12 +9,12 @@ "is_active": true, "is_superuser": false, "is_staff": true, - "last_login": "2010-08-14 01:51:05", + "last_login": "2010-08-14T01:51:05+00:00", "groups": [], "user_permissions": [], "password": "sha1$035cb$156ad6cb44332fb4f24bcb634142a67435be0b37", "email": "e@e.co", - "date_joined": "2010-08-14 01:50:58" + "date_joined": "2010-08-14T01:50:58+00:00" } }, { @@ -27,12 +27,12 @@ "is_active": true, "is_superuser": false, "is_staff": true, - "last_login": "2010-08-14 01:51:05", + "last_login": "2010-08-14T01:51:05+00:00", "groups": [], "user_permissions": [], "password": "sha1$035cb$156ad6cb44332fb4f24bcb634142a67435be0b37", "email": "e@etest.co", - "date_joined": "2010-08-14 01:50:58" + "date_joined": "2010-08-14T01:50:58+00:00" } }, { @@ -45,12 +45,12 @@ "is_active": true, "is_superuser": true, "is_staff": true, - "last_login": "2010-08-14 01:51:05", + "last_login": "2010-08-14T01:51:05+00:00", "groups": [], "user_permissions": [], "password": "sha1$035cb$156ad6cb44332fb4f24bcb634142a67435be0b37", "email": "e@e.co", - "date_joined": "2010-08-14 01:50:58" + "date_joined": "2010-08-14T01:50:58+00:00" } } ] diff --git a/readthedocs/core/management/commands/clean_builds.py b/readthedocs/core/management/commands/clean_builds.py index 93a3fada683..e47a651cf8a 100644 --- a/readthedocs/core/management/commands/clean_builds.py +++ b/readthedocs/core/management/commands/clean_builds.py @@ -1,12 +1,13 @@ """Clean up stable build paths per project version""" from __future__ import absolute_import -from datetime import datetime, timedelta +from datetime import timedelta import logging from optparse import make_option from django.core.management.base import BaseCommand from django.db.models import Max +from django.utils import timezone from readthedocs.builds.models import Build, Version @@ -35,7 +36,7 @@ def add_arguments(self, parser): def handle(self, *args, **options): """Find stale builds and remove build paths""" - max_date = datetime.now() - timedelta(days=options['days']) + max_date = timezone.now() - timedelta(days=options['days']) queryset = (Build.objects .values('project', 'version') .annotate(max_date=Max('date')) diff --git a/readthedocs/oauth/services/base.py b/readthedocs/oauth/services/base.py index 01239f4cbdf..93064779ef9 100644 --- a/readthedocs/oauth/services/base.py +++ b/readthedocs/oauth/services/base.py @@ -11,6 +11,7 @@ from allauth.socialaccount.providers import registry from builtins import object from django.conf import settings +from django.utils import timezone from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError from requests.exceptions import RequestException from requests_oauthlib import OAuth2Session @@ -83,7 +84,7 @@ def create_session(self): 'token_type': 'bearer', } if token.expires_at is not None: - token_expires = (token.expires_at - datetime.now()).total_seconds() + token_expires = (token.expires_at - timezone.now()).total_seconds() token_config.update({ 'refresh_token': token.token_secret, 'expires_in': token_expires, @@ -119,7 +120,9 @@ def token_updater(self, token): """ def _updater(data): token.token = data['access_token'] - token.expires_at = datetime.fromtimestamp(data['expires_at']) + token.expires_at = timezone.make_aware( + datetime.fromtimestamp(data['expires_at']) + ) token.save() log.info('Updated token %s:', token) diff --git a/readthedocs/projects/fixtures/test_auth.json b/readthedocs/projects/fixtures/test_auth.json index f76181fb96f..83d7738406e 100644 --- a/readthedocs/projects/fixtures/test_auth.json +++ b/readthedocs/projects/fixtures/test_auth.json @@ -675,12 +675,12 @@ "is_active": true, "is_superuser": false, "is_staff": false, - "last_login": "2014-02-09T19:47:26.625", + "last_login": "2014-02-09T19:47:26.625+00:00", "groups": [], "user_permissions": [], "password": "", "email": "", - "date_joined": "2014-02-09T19:47:26.625" + "date_joined": "2014-02-09T19:47:26.625+00:00" } }, { @@ -693,12 +693,12 @@ "is_active": true, "is_superuser": true, "is_staff": true, - "last_login": "2014-02-09T19:48:39.934", + "last_login": "2014-02-09T19:48:39.934+00:00", "groups": [], "user_permissions": [], "password": "pbkdf2_sha256$10000$FgAANNnclCS5$ElbS6laaFoh+nyHbEb96ICxS3xK1LioUS+CMQK+KdYM=", "email": "test@readthedocs.org", - "date_joined": "2014-02-09T19:48:39.934" + "date_joined": "2014-02-09T19:48:39.934+00:00" } } ] \ No newline at end of file diff --git a/readthedocs/projects/fixtures/test_data.json b/readthedocs/projects/fixtures/test_data.json index 34c4eaf5da3..a4f70a199a0 100644 --- a/readthedocs/projects/fixtures/test_data.json +++ b/readthedocs/projects/fixtures/test_data.json @@ -3,15 +3,15 @@ "pk": 1, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 14:36:46", + "modified_date": "2010-08-15T14:36:46+00:00", "description": "", "project_url": "", "repo": "https://github.com/rtfd/readthedocs.org", "version": "0.1", "users": [ - 1 - ], - "pub_date": "2010-08-14 01:37:58", + 1 + ], + "pub_date": "2010-08-14T01:37:58+00:00", "slug": "read-the-docs", "name": "Read The Docs" } @@ -20,15 +20,15 @@ "pk": 22, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:23", + "modified_date": "2010-08-15T13:18:23+00:00", "description": "", "project_url": "", "repo": "https://github.com/alex/django-taggit", "version": "0.8", "users": [ - 1 - ], - "pub_date": "2010-08-15 13:04:19", + 1 + ], + "pub_date": "2010-08-15T13:04:19+00:00", "slug": "taggit", "name": "Taggit" } @@ -37,15 +37,15 @@ "pk": 6, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:23", + "modified_date": "2010-08-15T13:18:23+00:00", "description": "", "project_url": "", "repo": "https://github.com/pypa/pip", "version": "", "users": [ - 1 - ], - "pub_date": "2010-08-14 11:08:12", + 1 + ], + "pub_date": "2010-08-14T11:08:12+00:00", "slug": "pip", "name": "Pip" } @@ -54,15 +54,15 @@ "pk": 10, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:22", + "modified_date": "2010-08-15T13:18:22+00:00", "description": "Awesome forms!", "project_url": "", "repo": "https://github.com/pydanny/django-uni-form", "version": "0.7", "users": [ - 1 - ], - "pub_date": "2010-08-14 13:34:59", + 1 + ], + "pub_date": "2010-08-14T13:34:59+00:00", "slug": "django-uni-form", "name": "Django Uni Form" } @@ -71,15 +71,15 @@ "pk": 9, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:21", + "modified_date": "2010-08-15T13:18:21+00:00", "description": "OMG API!", "project_url": "", "repo": "http://github.com/codysoyland/django-tastypie", "version": "0.8.2", "users": [ - 1 - ], - "pub_date": "2010-08-14 13:16:35", + 1 + ], + "pub_date": "2010-08-14T13:16:35+00:00", "slug": "tastypie", "name": "Tastypie" } @@ -88,15 +88,15 @@ "pk": 8, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:20", + "modified_date": "2010-08-15T13:18:20+00:00", "description": "ZOMG HE'S COMING?!", "project_url": "", "repo": "https://github.com/ericholscher/django-kong", "version": "0.1", "users": [ - 1 - ], - "pub_date": "2010-08-14 13:01:57", + 1 + ], + "pub_date": "2010-08-14T13:01:57+00:00", "slug": "kong", "name": "Kong" } @@ -105,15 +105,15 @@ "pk": 7, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:20", + "modified_date": "2010-08-15T13:18:20+00:00", "description": "", "project_url": "", "repo": "https://github.com/worldcompany/djangoembed", "version": "", "users": [ - 1 - ], - "pub_date": "2010-08-14 11:30:33", + 1 + ], + "pub_date": "2010-08-14T11:30:33+00:00", "slug": "djangoembed", "name": "Djangoembed" } @@ -122,15 +122,15 @@ "pk": 5, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:19", + "modified_date": "2010-08-15T13:18:19+00:00", "description": "", "project_url": "", "repo": "http://github.com/dmishe/django-south", "version": "0.7", "users": [ - 1 - ], - "pub_date": "2010-08-14 10:59:47", + 1 + ], + "pub_date": "2010-08-14T10:59:47+00:00", "slug": "south", "name": "South" } @@ -139,15 +139,15 @@ "pk": 4, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:19", + "modified_date": "2010-08-15T13:18:19+00:00", "description": "", "project_url": "", "repo": "https://github.com/fabric/fabric", "version": "0.1.0", "users": [ - 1 - ], - "pub_date": "2010-08-14 08:00:49", + 1 + ], + "pub_date": "2010-08-14T08:00:49+00:00", "slug": "fabric", "name": "Fabric" } @@ -156,15 +156,15 @@ "pk": 14, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:18", + "modified_date": "2010-08-15T13:18:18+00:00", "description": "", "project_url": "", "repo": "http://github.com/coleifer/cue.git", "version": "0.1.0", "users": [ - 1 - ], - "pub_date": "2010-08-14 23:19:49", + 1 + ], + "pub_date": "2010-08-14T23:19:49+00:00", "slug": "cue", "name": "Cue" } @@ -173,15 +173,15 @@ "pk": 16, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:17", + "modified_date": "2010-08-15T13:18:17+00:00", "description": "Awesome", "project_url": "", "repo": "http://github.com/pinax/pinax", "version": "0.9", "users": [ - 1 - ], - "pub_date": "2010-08-15 00:12:14", + 1 + ], + "pub_date": "2010-08-15T00:12:14+00:00", "slug": "pinax", "name": "Pinax" } @@ -190,15 +190,15 @@ "pk": 2, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:16", + "modified_date": "2010-08-15T13:18:16+00:00", "description": "", "project_url": "", "repo": "https://github.com/ericholscher/django-test-utils", "version": "0.2", "users": [ - 1 - ], - "pub_date": "2010-08-14 02:16:35", + 1 + ], + "pub_date": "2010-08-14T02:16:35+00:00", "slug": "django-test-utils", "name": "Django Test Utils" } @@ -278,27 +278,27 @@ "is_active": true, "is_superuser": true, "is_staff": true, - "last_login": "2010-08-14 01:51:05", + "last_login": "2010-08-14T01:51:05+00:00", "groups": [], "user_permissions": [], "password": "sha1$efaa6$17551368b198ef0dffcbf388908b7a609ec22eb1", "email": "e@etest.co", - "date_joined": "2010-08-14 01:50:58" + "date_joined": "2010-08-14T01:50:58+00:00" } }, { "pk": 17, "model": "projects.project", "fields": { - "modified_date": "2010-08-15 13:18:14", + "modified_date": "2010-08-15T13:18:14+00:00", "description": "Awesome docs", "project_url": "", "repo": "https://github.com/toastdriven/django-haystack", "version": "1.1.0", "users": [ - 1 - ], - "pub_date": "2010-08-15 01:11:22", + 1 + ], + "pub_date": "2010-08-15T01:11:22+00:00", "slug": "haystack", "name": "Haystack" } @@ -307,12 +307,12 @@ "pk": 23, "model": "projects.project", "fields": { - "modified_date": "2014-09-10T11:58:01.277", + "modified_date": "2014-09-10T11:58:01.277+00:00", "description": "", "project_url": "", "repo": "https://github.com/bogususer/non-existing-repo", "version": "", - "pub_date": "2014-09-10 11:58:01", + "pub_date": "2014-09-10T11:58:01+00:00", "slug": "test_project", "name": "test_project" } diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index d781311d9eb..9dcc349edba 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -25,6 +25,7 @@ from django.core.urlresolvers import reverse from django.db.models import Q from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone from readthedocs.config import ConfigError from slumber.exceptions import HttpClientError @@ -1220,7 +1221,7 @@ def finish_inactive_builds(): time_limit = int(DOCKER_LIMITS['time'] * 1.2) delta = datetime.timedelta(seconds=time_limit) query = (~Q(state=BUILD_STATE_FINISHED) & - Q(date__lte=datetime.datetime.now() - delta)) + Q(date__lte=timezone.now() - delta)) builds_finished = 0 builds = Build.objects.filter(query)[:50] @@ -1229,7 +1230,7 @@ def finish_inactive_builds(): if build.project.container_time_limit: custom_delta = datetime.timedelta( seconds=int(build.project.container_time_limit)) - if build.date + custom_delta > datetime.datetime.now(): + if build.date + custom_delta > timezone.now(): # Do not mark as FINISHED builds with a custom time limit that wasn't # expired yet (they are still building the project version) continue diff --git a/readthedocs/projects/views/base.py b/readthedocs/projects/views/base.py index 3db095f9eaf..c7323f9512b 100644 --- a/readthedocs/projects/views/base.py +++ b/readthedocs/projects/views/base.py @@ -5,12 +5,13 @@ import logging from builtins import object -from datetime import datetime, timedelta +from datetime import timedelta from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 +from django.utils import timezone from ..exceptions import ProjectSpamError from ..models import Project @@ -101,7 +102,7 @@ def post(self, request, *args, **kwargs): try: return super(ProjectSpamMixin, self).post(request, *args, **kwargs) except ProjectSpamError: - date_maturity = datetime.now() - timedelta(days=USER_MATURITY_DAYS) + date_maturity = timezone.now() - timedelta(days=USER_MATURITY_DAYS) if request.user.date_joined > date_maturity: request.user.profile.banned = True request.user.profile.save() diff --git a/readthedocs/rtd_tests/tests/test_project.py b/readthedocs/rtd_tests/tests/test_project.py index e4d5b371af3..3bdc34b1917 100644 --- a/readthedocs/rtd_tests/tests/test_project.py +++ b/readthedocs/rtd_tests/tests/test_project.py @@ -9,6 +9,7 @@ from django.forms.models import model_to_dict from django.test import TestCase from django_dynamic_fixture import get +from django.utils import timezone from mock import patch from rest_framework.reverse import reverse @@ -435,7 +436,7 @@ def setUp(self): state=BUILD_STATE_TRIGGERED, ) self.build_2.date = ( - datetime.datetime.now() - datetime.timedelta(hours=1)) + timezone.now() - datetime.timedelta(hours=1)) self.build_2.save() # Build started an hour ago with custom time (2 hours) @@ -445,7 +446,7 @@ def setUp(self): state=BUILD_STATE_TRIGGERED, ) self.build_3.date = ( - datetime.datetime.now() - datetime.timedelta(hours=1)) + timezone.now() - datetime.timedelta(hours=1)) self.build_3.save() def test_finish_inactive_builds_task(self): diff --git a/readthedocs/rtd_tests/tests/test_project_views.py b/readthedocs/rtd_tests/tests/test_project_views.py index 0615bf4aed9..16dcc33c587 100644 --- a/readthedocs/rtd_tests/tests/test_project_views.py +++ b/readthedocs/rtd_tests/tests/test_project_views.py @@ -1,5 +1,6 @@ from __future__ import absolute_import -from datetime import datetime, timedelta +from datetime import timedelta + from mock import patch from django.test import TestCase @@ -9,6 +10,7 @@ from django.core.urlresolvers import reverse from django.http.response import HttpResponseRedirect from django.views.generic.base import ContextMixin +from django.utils import timezone from django_dynamic_fixture import get from django_dynamic_fixture import new @@ -207,7 +209,7 @@ def test_remote_repository_is_added(self): create=True) def test_form_spam(self, mocked_validator): """Don't add project on a spammy description""" - self.user.date_joined = datetime.now() - timedelta(days=365) + self.user.date_joined = timezone.now() - timedelta(days=365) self.user.save() mocked_validator.side_effect = ProjectSpamError @@ -229,7 +231,7 @@ def test_form_spam(self, mocked_validator): create=True) def test_form_spam_ban_user(self, mocked_validator): """Don't add spam and ban new user""" - self.user.date_joined = datetime.now() + self.user.date_joined = timezone.now() self.user.save() mocked_validator.side_effect = ProjectSpamError diff --git a/readthedocs/search/indexes.py b/readthedocs/search/indexes.py index 19c4b2ba772..48e4baecc5e 100644 --- a/readthedocs/search/indexes.py +++ b/readthedocs/search/indexes.py @@ -16,7 +16,8 @@ """ from __future__ import absolute_import from builtins import object -import datetime + +from django.utils import timezone from elasticsearch import Elasticsearch, exceptions from elasticsearch.helpers import bulk_index @@ -92,7 +93,7 @@ def get_analysis(self): def timestamped_index(self): return '{0}-{1}'.format( - self._index, datetime.datetime.now().strftime('%Y%m%d%H%M%S')) + self._index, timezone.now().strftime('%Y%m%d%H%M%S')) def create_index(self, index=None): """ diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index a9113c7ee08..75ff826ba23 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -215,7 +215,8 @@ def USE_PROMOS(self): # noqa CACHE_MIDDLEWARE_SECONDS = 60 # I18n - TIME_ZONE = 'America/Chicago' + TIME_ZONE = 'UTC' + USE_TZ = True LANGUAGE_CODE = 'en-us' LANGUAGES = ( ('ca', gettext('Catalan')), From ad7c76cd893a5f4c26a3a32fc69eb841642b5fb1 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 21 Aug 2018 12:54:05 -0700 Subject: [PATCH 04/31] Found one more naive datetime --- readthedocs/core/admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/readthedocs/core/admin.py b/readthedocs/core/admin.py index 5dbc3335dd9..b30f5460484 100644 --- a/readthedocs/core/admin.py +++ b/readthedocs/core/admin.py @@ -1,12 +1,13 @@ """Django admin interface for core models.""" from __future__ import absolute_import -from datetime import datetime, timedelta +from datetime import timedelta from django.contrib import admin from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone from readthedocs.core.models import UserProfile from readthedocs.projects.models import Project @@ -50,7 +51,7 @@ def queryset(self, request, queryset): if self.value() == self.PROJECT_BUILT: return queryset.filter(projects__versions__built=True) if self.value() == self.PROJECT_RECENT: - recent_date = datetime.today() - timedelta(days=365) + recent_date = timezone.now() - timedelta(days=365) return queryset.filter(projects__builds__date__gt=recent_date) From 0a01b967a6af6ea077bedd00cc3dcb38ff13faed Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 18 Sep 2018 15:38:00 -0700 Subject: [PATCH 05/31] Fix a merge fail --- readthedocs/projects/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index eab828f210c..a76e9a9c2c6 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -27,7 +27,6 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from readthedocs.config import ConfigError from slumber.exceptions import HttpClientError from readthedocs.builds.constants import ( From 1bcc0700a5cd6d38a07eb6b5e92962fe3a590ff6 Mon Sep 17 00:00:00 2001 From: Rahul Tiwari Date: Sat, 27 Oct 2018 03:22:00 +0530 Subject: [PATCH 06/31] Add MkDocsYAMLParseError --- readthedocs/doc_builder/backends/mkdocs.py | 4 ++-- readthedocs/doc_builder/environments.py | 3 ++- readthedocs/doc_builder/exceptions.py | 13 ++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/readthedocs/doc_builder/backends/mkdocs.py b/readthedocs/doc_builder/backends/mkdocs.py index eaa388cad7b..6119673b8fa 100644 --- a/readthedocs/doc_builder/backends/mkdocs.py +++ b/readthedocs/doc_builder/backends/mkdocs.py @@ -15,7 +15,7 @@ from django.template import loader as template_loader from readthedocs.doc_builder.base import BaseBuilder -from readthedocs.doc_builder.exceptions import BuildEnvironmentError +from readthedocs.doc_builder.exceptions import MkDocsYAMLParseError from readthedocs.projects.models import Feature log = logging.getLogger(__name__) @@ -99,7 +99,7 @@ def load_yaml_config(self): if hasattr(exc, 'problem_mark'): mark = exc.problem_mark note = ' (line %d, column %d)' % (mark.line + 1, mark.column + 1) - raise BuildEnvironmentError( + raise MkDocsYAMLParseError( 'Your mkdocs.yml could not be loaded, ' 'possibly due to a syntax error{note}'.format(note=note) ) diff --git a/readthedocs/doc_builder/environments.py b/readthedocs/doc_builder/environments.py index 575dd1c76ed..038aeb0d834 100644 --- a/readthedocs/doc_builder/environments.py +++ b/readthedocs/doc_builder/environments.py @@ -37,7 +37,7 @@ from .exceptions import ( BuildEnvironmentCreationFailed, BuildEnvironmentError, BuildEnvironmentException, BuildEnvironmentWarning, BuildTimeoutError, - ProjectBuildsSkippedError, VersionLockedError, YAMLParseError) + ProjectBuildsSkippedError, VersionLockedError, YAMLParseError, MkDocsYAMLParseError) log = logging.getLogger(__name__) @@ -438,6 +438,7 @@ class BuildEnvironment(BaseEnvironment): ProjectBuildsSkippedError, YAMLParseError, BuildTimeoutError, + MkDocsYAMLParseError ) def __init__(self, project=None, version=None, build=None, config=None, diff --git a/readthedocs/doc_builder/exceptions.py b/readthedocs/doc_builder/exceptions.py index 360e2845256..391b43b69cd 100644 --- a/readthedocs/doc_builder/exceptions.py +++ b/readthedocs/doc_builder/exceptions.py @@ -7,7 +7,6 @@ class BuildEnvironmentException(Exception): - message = None status_code = None @@ -21,7 +20,6 @@ def get_default_message(self): class BuildEnvironmentError(BuildEnvironmentException): - GENERIC_WITH_BUILD_ID = ugettext_noop( 'There was a problem with Read the Docs while building your documentation. ' 'Please report this to us with your build id ({build_id}).', @@ -29,32 +27,33 @@ class BuildEnvironmentError(BuildEnvironmentException): class BuildEnvironmentCreationFailed(BuildEnvironmentError): - message = ugettext_noop('Build environment creation failed') class VersionLockedError(BuildEnvironmentError): - message = ugettext_noop('Version locked, retrying in 5 minutes.') status_code = 423 class ProjectBuildsSkippedError(BuildEnvironmentError): - message = ugettext_noop('Builds for this project are temporarily disabled') class YAMLParseError(BuildEnvironmentError): - GENERIC_WITH_PARSE_EXCEPTION = ugettext_noop( 'Problem parsing YAML configuration. {exception}', ) class BuildTimeoutError(BuildEnvironmentError): - message = ugettext_noop('Build exited due to time out') class BuildEnvironmentWarning(BuildEnvironmentException): pass + + +class MkDocsYAMLParseError(BuildEnvironmentError): + GENERIC_WITH_PARSE_EXCEPTION = ugettext_noop( + 'Problem parsing MkDocs YAML configuration. {exception}', + ) From fcb730c463fbcc5de1b3fccc248ee0d688709118 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 31 Oct 2018 11:08:57 -0500 Subject: [PATCH 07/31] Fail on warning --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cc48c77cb94..3735b7d0b6f 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ commands = description = build readthedocs documentation changedir = {toxinidir}/docs commands = - sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html + sphinx-build -b html -W -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:docs-lint] description = run linter (rstcheck) to ensure there aren't errors on our docs From 858eb20edc9e9e6800fd9f2f9546c07922da452e Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 31 Oct 2018 11:09:10 -0500 Subject: [PATCH 08/31] Add rtd config file --- .readthedocs.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000000..09e7f891686 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,8 @@ +version: 2 +formats: all +sphinx: + configuration: docs/conf.py + fail_on_warning: true +python: + install: + requirements: requirements.txt From 5995bf81630b2feb64b7b2ee770b70c441fff51d Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 31 Oct 2018 11:17:13 -0500 Subject: [PATCH 09/31] Don't fail on warning --- .readthedocs.yml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 09e7f891686..3debbab6f59 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,6 @@ version: 2 formats: all sphinx: configuration: docs/conf.py - fail_on_warning: true python: install: requirements: requirements.txt diff --git a/tox.ini b/tox.ini index 3735b7d0b6f..cc48c77cb94 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ commands = description = build readthedocs documentation changedir = {toxinidir}/docs commands = - sphinx-build -b html -W -d {envtmpdir}/doctrees . {envtmpdir}/html + sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:docs-lint] description = run linter (rstcheck) to ensure there aren't errors on our docs From 262cde986538b68d1cc6d2de57368f07b00d6b0d Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Thu, 1 Nov 2018 14:37:07 -0500 Subject: [PATCH 10/31] Add modified_date to ImportedFile. This makes it easier to see when files have been updated so we can reindex smarter --- .../0030_add_modified_date_importedfile.py | 20 +++++++++++++++++++ readthedocs/projects/models.py | 1 + 2 files changed, 21 insertions(+) create mode 100644 readthedocs/projects/migrations/0030_add_modified_date_importedfile.py diff --git a/readthedocs/projects/migrations/0030_add_modified_date_importedfile.py b/readthedocs/projects/migrations/0030_add_modified_date_importedfile.py new file mode 100644 index 00000000000..3ad50c93bc7 --- /dev/null +++ b/readthedocs/projects/migrations/0030_add_modified_date_importedfile.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2018-11-01 14:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0029_add_additional_languages'), + ] + + operations = [ + migrations.AddField( + model_name='importedfile', + name='modified_date', + field=models.DateTimeField(auto_now=True, verbose_name='Modified date'), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 9d5c3855f35..2c1cab4420d 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -943,6 +943,7 @@ class ImportedFile(models.Model): path = models.CharField(_('Path'), max_length=255) md5 = models.CharField(_('MD5 checksum'), max_length=255) commit = models.CharField(_('Commit'), max_length=255) + modified_date = models.DateTimeField(_('Modified date'), auto_now=True) def get_absolute_url(self): return resolve(project=self.project, version_slug=self.version.slug, filename=self.path) From 4bb8154218271f28d2406aead79a49b360c93768 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 15:52:29 -0500 Subject: [PATCH 11/31] Tests --- readthedocs/rtd_tests/tests/test_footer.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/readthedocs/rtd_tests/tests/test_footer.py b/readthedocs/rtd_tests/tests/test_footer.py index 82e1b780b19..2b8ec58d387 100644 --- a/readthedocs/rtd_tests/tests/test_footer.py +++ b/readthedocs/rtd_tests/tests/test_footer.py @@ -3,6 +3,7 @@ absolute_import, division, print_function, unicode_literals) import mock +from django_dynamic_fixture import get from django.test import TestCase from rest_framework.test import APIRequestFactory, APITestCase @@ -17,7 +18,7 @@ class Testmaker(APITestCase): fixtures = ['test_data'] - url = '/api/v2/footer_html/?project=pip&version=latest&page=index' + url = '/api/v2/footer_html/?project=pip&version=latest&page=index&docroot=/' factory = APIRequestFactory() @classmethod @@ -99,6 +100,24 @@ def test_show_version_warning(self): response = self.render() self.assertTrue(response.data['show_version_warning']) + def test_show_edit_on_github(self): + version = self.pip.versions.get(slug=LATEST) + version.type = BRANCH + version.save() + response = self.render() + self.assertIn('On GitHub', response.data['html']) + self.assertIn('View', response.data['html']) + self.assertIn('Edit', response.data['html']) + + def test_not_show_edit_on_github(self): + version = self.pip.versions.get(slug=LATEST) + version.type = TAG + version.save() + response = self.render() + self.assertIn('On GitHub', response.data['html']) + self.assertIn('View', response.data['html']) + self.assertNotIn('Edit', response.data['html']) + class TestVersionCompareFooter(TestCase): fixtures = ['test_data'] From 531fda743802fea4c04212180855074f3cd6d5b5 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 16:00:30 -0500 Subject: [PATCH 12/31] Isort --- readthedocs/rtd_tests/tests/test_footer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/readthedocs/rtd_tests/tests/test_footer.py b/readthedocs/rtd_tests/tests/test_footer.py index 2b8ec58d387..2b63301753f 100644 --- a/readthedocs/rtd_tests/tests/test_footer.py +++ b/readthedocs/rtd_tests/tests/test_footer.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import ( - absolute_import, division, print_function, unicode_literals) + absolute_import, + division, + print_function, + unicode_literals, +) import mock -from django_dynamic_fixture import get from django.test import TestCase from rest_framework.test import APIRequestFactory, APITestCase @@ -12,7 +15,9 @@ from readthedocs.core.middleware import FooterNoSessionMiddleware from readthedocs.projects.models import Project from readthedocs.restapi.views.footer_views import ( - footer_html, get_version_compare_data) + footer_html, + get_version_compare_data, +) from readthedocs.rtd_tests.mocks.paths import fake_paths_by_regex From 8c424cc65b80d7471e82df7cc1a61f990f166216 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 16:04:03 -0500 Subject: [PATCH 13/31] Solution --- readthedocs/builds/models.py | 4 ++++ readthedocs/restapi/templates/restapi/footer.html | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index ffd15ee6844..8f6b17c57f1 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -193,6 +193,10 @@ def identifier_friendly(self): return self.identifier[:8] return self.identifier + @property + def is_editable(self): + return self.type == BRANCH + def get_subdomain_url(self): private = self.privacy_level == PRIVATE return self.project.get_docs_url( diff --git a/readthedocs/restapi/templates/restapi/footer.html b/readthedocs/restapi/templates/restapi/footer.html index 9a995a794f4..747f2c611f2 100644 --- a/readthedocs/restapi/templates/restapi/footer.html +++ b/readthedocs/restapi/templates/restapi/footer.html @@ -82,9 +82,11 @@
View
+ {% if version.is_editable %}
Edit
+ {% endif %} {% elif bitbucket_url %}
@@ -99,9 +101,11 @@
View
+ {% if version.is_editable %}
Edit
+ {% endif %}
{% endif %} {% endblock %} From 48ac0f408255171f5d78b5e58d7a8c949230b155 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 16:04:41 -0500 Subject: [PATCH 14/31] Isort --- readthedocs/builds/models.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 8f6b17c57f1..ec2825e44e4 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -2,36 +2,55 @@ """Models for the builds app.""" from __future__ import ( - absolute_import, division, print_function, unicode_literals) + absolute_import, + division, + print_function, + unicode_literals, +) import logging import os.path import re -from builtins import object from shutil import rmtree +from builtins import object from django.conf import settings from django.core.urlresolvers import reverse from django.db import models from django.utils.encoding import python_2_unicode_compatible -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext +from django.utils.translation import ugettext_lazy as _ from guardian.shortcuts import assign from taggit.managers import TaggableManager from readthedocs.core.utils import broadcast from readthedocs.projects.constants import ( - BITBUCKET_URL, GITHUB_URL, GITLAB_URL, PRIVACY_CHOICES, PRIVATE) + BITBUCKET_URL, + GITHUB_URL, + GITLAB_URL, + PRIVACY_CHOICES, + PRIVATE, +) from readthedocs.projects.models import APIProject, Project from .constants import ( - BRANCH, BUILD_STATE, BUILD_STATE_FINISHED, BUILD_TYPES, LATEST, - NON_REPOSITORY_VERSIONS, STABLE, TAG, VERSION_TYPES) + BRANCH, + BUILD_STATE, + BUILD_STATE_FINISHED, + BUILD_TYPES, + LATEST, + NON_REPOSITORY_VERSIONS, + STABLE, + TAG, + VERSION_TYPES, +) from .managers import VersionManager from .querysets import BuildQuerySet, RelatedBuildQuerySet, VersionQuerySet from .utils import ( - get_bitbucket_username_repo, get_github_username_repo, - get_gitlab_username_repo) + get_bitbucket_username_repo, + get_github_username_repo, + get_gitlab_username_repo, +) from .version_slug import VersionSlugField DEFAULT_VERSION_PRIVACY_LEVEL = getattr( From dac82763a239a864ae70b85c912ff9cb34d91706 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Fri, 29 Dec 2017 16:43:23 -0500 Subject: [PATCH 15/31] Set slug max length to 63 A valid DNS label can contain up to 63 characters Closes #2600 --- readthedocs/projects/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 9d5c3855f35..ca96ae1de3b 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -81,7 +81,8 @@ class Project(models.Model): users = models.ManyToManyField(User, verbose_name=_('User'), related_name='projects') name = models.CharField(_('Name'), max_length=255) - slug = models.SlugField(_('Slug'), max_length=255, unique=True) + # A DNS label can contain up to 63 characters. + slug = models.SlugField(_('Slug'), max_length=63, unique=True) description = models.TextField(_('Description'), blank=True, help_text=_('The reStructuredText ' 'description of the project')) From 5177ad64d14c638d956ca4506175dfd6062f79e8 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Fri, 29 Dec 2017 16:46:57 -0500 Subject: [PATCH 16/31] Add project migrations --- .../migrations/0024_auto_20171229_1546.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 readthedocs/projects/migrations/0024_auto_20171229_1546.py diff --git a/readthedocs/projects/migrations/0024_auto_20171229_1546.py b/readthedocs/projects/migrations/0024_auto_20171229_1546.py new file mode 100644 index 00000000000..8ac5d46ccc8 --- /dev/null +++ b/readthedocs/projects/migrations/0024_auto_20171229_1546.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2017-12-29 15:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0023_migrate-alias-slug'), + ] + + operations = [ + migrations.AlterField( + model_name='domain', + name='canonical', + field=models.BooleanField(default=False, help_text='This Domain is the primary one where the documentation is served from'), + ), + migrations.AlterField( + model_name='domain', + name='count', + field=models.IntegerField(default=0, help_text='Number of times this domain has been hit'), + ), + migrations.AlterField( + model_name='project', + name='allow_promos', + field=models.BooleanField(default=True, help_text='If unchecked, users will still see community ads.', verbose_name='Allow paid advertising'), + ), + migrations.AlterField( + model_name='project', + name='comment_moderation', + field=models.BooleanField(default=False, verbose_name='Comment Moderation'), + ), + migrations.AlterField( + model_name='project', + name='conf_py_file', + field=models.CharField(blank=True, default='', help_text='Path from project root to conf.py file (ex. docs/conf.py). Leave blank if you want us to find it for you.', max_length=255, verbose_name='Python configuration file'), + ), + migrations.AlterField( + model_name='project', + name='has_valid_webhook', + field=models.BooleanField(default=False, help_text='This project has been built with a webhook'), + ), + migrations.AlterField( + model_name='project', + name='programming_language', + field=models.CharField(blank=True, choices=[('words', 'Only Words'), ('py', 'Python'), ('js', 'JavaScript'), ('php', 'PHP'), ('ruby', 'Ruby'), ('perl', 'Perl'), ('java', 'Java'), ('go', 'Go'), ('julia', 'Julia'), ('c', 'C'), ('csharp', 'C#'), ('cpp', 'C++'), ('objc', 'Objective-C'), ('other', 'Other')], default='words', help_text='The primary programming language the project is written in.', max_length=20, verbose_name='Programming Language'), + ), + migrations.AlterField( + model_name='project', + name='slug', + field=models.SlugField(max_length=63, unique=True, verbose_name='Slug'), + ), + ] From d71bb7f8bd6d526b7c3e2145794e79ae97757abb Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 21:36:13 -0500 Subject: [PATCH 17/31] Add migration --- .../migrations/0024_auto_20171229_1546.py | 55 ------------------- .../0030_change-max-length-project-slug.py | 36 ++++++++++++ 2 files changed, 36 insertions(+), 55 deletions(-) delete mode 100644 readthedocs/projects/migrations/0024_auto_20171229_1546.py create mode 100644 readthedocs/projects/migrations/0030_change-max-length-project-slug.py diff --git a/readthedocs/projects/migrations/0024_auto_20171229_1546.py b/readthedocs/projects/migrations/0024_auto_20171229_1546.py deleted file mode 100644 index 8ac5d46ccc8..00000000000 --- a/readthedocs/projects/migrations/0024_auto_20171229_1546.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.12 on 2017-12-29 15:46 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0023_migrate-alias-slug'), - ] - - operations = [ - migrations.AlterField( - model_name='domain', - name='canonical', - field=models.BooleanField(default=False, help_text='This Domain is the primary one where the documentation is served from'), - ), - migrations.AlterField( - model_name='domain', - name='count', - field=models.IntegerField(default=0, help_text='Number of times this domain has been hit'), - ), - migrations.AlterField( - model_name='project', - name='allow_promos', - field=models.BooleanField(default=True, help_text='If unchecked, users will still see community ads.', verbose_name='Allow paid advertising'), - ), - migrations.AlterField( - model_name='project', - name='comment_moderation', - field=models.BooleanField(default=False, verbose_name='Comment Moderation'), - ), - migrations.AlterField( - model_name='project', - name='conf_py_file', - field=models.CharField(blank=True, default='', help_text='Path from project root to conf.py file (ex. docs/conf.py). Leave blank if you want us to find it for you.', max_length=255, verbose_name='Python configuration file'), - ), - migrations.AlterField( - model_name='project', - name='has_valid_webhook', - field=models.BooleanField(default=False, help_text='This project has been built with a webhook'), - ), - migrations.AlterField( - model_name='project', - name='programming_language', - field=models.CharField(blank=True, choices=[('words', 'Only Words'), ('py', 'Python'), ('js', 'JavaScript'), ('php', 'PHP'), ('ruby', 'Ruby'), ('perl', 'Perl'), ('java', 'Java'), ('go', 'Go'), ('julia', 'Julia'), ('c', 'C'), ('csharp', 'C#'), ('cpp', 'C++'), ('objc', 'Objective-C'), ('other', 'Other')], default='words', help_text='The primary programming language the project is written in.', max_length=20, verbose_name='Programming Language'), - ), - migrations.AlterField( - model_name='project', - name='slug', - field=models.SlugField(max_length=63, unique=True, verbose_name='Slug'), - ), - ] diff --git a/readthedocs/projects/migrations/0030_change-max-length-project-slug.py b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py new file mode 100644 index 00000000000..a166a50fca6 --- /dev/null +++ b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2018-11-01 20:55 +from __future__ import unicode_literals + +from django.db import migrations, models +from django.db.models.functions import Length + + +def forwards_func(apps, schema_editor): + max_length = 63 + Project = apps.get_model('projects', 'Project') + projects_invalid_slug = ( + Project + .objects + .annotate(slug_length=Length('slug')) + .filter(slug_length__gt=max_length) + ) + for project in projects_invalid_slug: + project.slug = project.slug[:max_length] + project.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0029_add_additional_languages'), + ] + + operations = [ + migrations.RunPython(forwards_func), + migrations.AlterField( + model_name='project', + name='slug', + field=models.SlugField(max_length=63, unique=True, verbose_name='Slug'), + ), + ] From 69eb9f42e3fd7fcebe32990d4d30309f29fdf95b Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 22:45:39 -0500 Subject: [PATCH 18/31] Use $HOME as CWD for virtualenv creation Closes #4808 --- readthedocs/doc_builder/python_environments.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 7d3a4c0e86d..882a0ebbbc5 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -215,8 +215,10 @@ def setup_base(self): site_packages, '--no-download', env_path, - bin_path=None, # Don't use virtualenv bin that doesn't exist yet - cwd=self.checkout_path, + # Don't use virtualenv bin that doesn't exist yet + bin_path=None, + # Don't use the project's root, some config files can interfere + cwd='$HOME', ) def install_core_requirements(self): From 9be18f99af5f62ea193b41413184b9806f0a925f Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Thu, 1 Nov 2018 23:16:13 -0500 Subject: [PATCH 19/31] Generic message for parser error of config file --- readthedocs/doc_builder/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/doc_builder/exceptions.py b/readthedocs/doc_builder/exceptions.py index 4897fd41daa..ce2ce3d844b 100644 --- a/readthedocs/doc_builder/exceptions.py +++ b/readthedocs/doc_builder/exceptions.py @@ -43,7 +43,7 @@ class ProjectBuildsSkippedError(BuildEnvironmentError): class YAMLParseError(BuildEnvironmentError): GENERIC_WITH_PARSE_EXCEPTION = ugettext_noop( - 'Problem parsing YAML configuration. {exception}', + 'Problem in your project\'s configuration. {exception}', ) From 8d2ee29de95690a9cd72d4f2a0b4131a44449928 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Fri, 2 Nov 2018 11:12:53 -0500 Subject: [PATCH 20/31] Shorten project name to match slug length This updates the just-merged migration to handle name as well --- .../0030_change-max-length-project-slug.py | 15 +++++++++++++++ readthedocs/projects/models.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/readthedocs/projects/migrations/0030_change-max-length-project-slug.py b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py index a166a50fca6..e21e13f498f 100644 --- a/readthedocs/projects/migrations/0030_change-max-length-project-slug.py +++ b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py @@ -19,6 +19,16 @@ def forwards_func(apps, schema_editor): project.slug = project.slug[:max_length] project.save() + projects_invalid_name = ( + Project + .objects + .annotate(name_length=Length('name')) + .filter(name_length__gt=max_length) + ) + for project in projects_invalid_name: + project.name = project.name[:max_length] + project.save() + class Migration(migrations.Migration): @@ -33,4 +43,9 @@ class Migration(migrations.Migration): name='slug', field=models.SlugField(max_length=63, unique=True, verbose_name='Slug'), ), + migrations.AlterField( + model_name='project', + name='name', + field=models.SlugField(max_length=63, verbose_name='Name'), + ), ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index ca96ae1de3b..5a051f03431 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -80,7 +80,7 @@ class Project(models.Model): # Generally from conf.py users = models.ManyToManyField(User, verbose_name=_('User'), related_name='projects') - name = models.CharField(_('Name'), max_length=255) + name = models.CharField(_('Name'), max_length=63) # A DNS label can contain up to 63 characters. slug = models.SlugField(_('Slug'), max_length=63, unique=True) description = models.TextField(_('Description'), blank=True, From 6bfbce5c9091fd7784a841f78599e9adcaa642ad Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Fri, 2 Nov 2018 14:06:39 -0500 Subject: [PATCH 21/31] Fix review feedback --- .../projects/migrations/0030_change-max-length-project-slug.py | 2 +- readthedocs/projects/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/projects/migrations/0030_change-max-length-project-slug.py b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py index e21e13f498f..7e9b48da270 100644 --- a/readthedocs/projects/migrations/0030_change-max-length-project-slug.py +++ b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py @@ -46,6 +46,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='project', name='name', - field=models.SlugField(max_length=63, verbose_name='Name'), + field=models.CharField(max_length=63, verbose_name='Name'), ), ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 5a051f03431..94451aa69e2 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -80,8 +80,8 @@ class Project(models.Model): # Generally from conf.py users = models.ManyToManyField(User, verbose_name=_('User'), related_name='projects') - name = models.CharField(_('Name'), max_length=63) # A DNS label can contain up to 63 characters. + name = models.CharField(_('Name'), max_length=63) slug = models.SlugField(_('Slug'), max_length=63, unique=True) description = models.TextField(_('Description'), blank=True, help_text=_('The reStructuredText ' From 8e1f378af2d1198ab093983e58889c791dea095c Mon Sep 17 00:00:00 2001 From: Rahul Tiwari Date: Sat, 8 Sep 2018 13:15:23 +0530 Subject: [PATCH 22/31] Redirect to build detail post manual build --- readthedocs/builds/views.py | 49 +++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index 473db6ac245..e02b707b511 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -1,26 +1,32 @@ +# -*- coding: utf-8 -*- """Views for builds app.""" -from __future__ import absolute_import -from builtins import object +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + import logging -from django.shortcuts import get_object_or_404 -from django.views.generic import ListView, DetailView +from builtins import object +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse from django.http import ( HttpResponseForbidden, HttpResponsePermanentRedirect, HttpResponseRedirect, ) -from django.contrib.auth.decorators import login_required -from readthedocs.core.permissions import AdminPermission -from django.core.urlresolvers import reverse +from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator +from django.views.generic import DetailView, ListView from readthedocs.builds.models import Build, Version +from readthedocs.core.permissions import AdminPermission from readthedocs.core.utils import trigger_build from readthedocs.projects.models import Project - log = logging.getLogger(__name__) @@ -31,9 +37,11 @@ def get_queryset(self): self.project_slug = self.kwargs.get('project_slug', None) self.project = get_object_or_404( Project.objects.protected(self.request.user), - slug=self.project_slug + slug=self.project_slug, + ) + queryset = Build.objects.public( + user=self.request.user, project=self.project ) - queryset = Build.objects.public(user=self.request.user, project=self.project) return queryset @@ -55,7 +63,10 @@ def post(self, request, project_slug): ) trigger_build(project=project, version=version) - return HttpResponseRedirect(reverse('builds_project_list', args=[project.slug])) + build_pk = project.builds.first().pk + return HttpResponseRedirect( + reverse('builds_detail', args=[project.slug, build_pk]) + ) class BuildList(BuildBase, BuildTriggerMixin, ListView): @@ -63,11 +74,14 @@ class BuildList(BuildBase, BuildTriggerMixin, ListView): def get_context_data(self, **kwargs): context = super(BuildList, self).get_context_data(**kwargs) - active_builds = self.get_queryset().exclude(state="finished").values('id') + active_builds = self.get_queryset().exclude(state='finished' + ).values('id') context['project'] = self.project context['active_builds'] = active_builds - context['versions'] = Version.objects.public(user=self.request.user, project=self.project) + context['versions'] = Version.objects.public( + user=self.request.user, project=self.project + ) context['build_qs'] = self.get_queryset() return context @@ -84,9 +98,14 @@ def get_context_data(self, **kwargs): # Old build view redirects + def builds_redirect_list(request, project_slug): # pylint: disable=unused-argument - return HttpResponsePermanentRedirect(reverse('builds_project_list', args=[project_slug])) + return HttpResponsePermanentRedirect( + reverse('builds_project_list', args=[project_slug]) + ) def builds_redirect_detail(request, project_slug, pk): # pylint: disable=unused-argument - return HttpResponsePermanentRedirect(reverse('builds_detail', args=[project_slug, pk])) + return HttpResponsePermanentRedirect( + reverse('builds_detail', args=[project_slug, pk]) + ) From b380c90fe95392a68d13987755c810c54aa3969c Mon Sep 17 00:00:00 2001 From: Rahul Tiwari Date: Mon, 1 Oct 2018 11:20:59 +0530 Subject: [PATCH 23/31] Return signature from trigger build and use it to fetch build_pk --- readthedocs/builds/views.py | 4 ++-- readthedocs/core/utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index e02b707b511..8118d788d45 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -62,8 +62,8 @@ def post(self, request, project_slug): slug=version_slug, ) - trigger_build(project=project, version=version) - build_pk = project.builds.first().pk + signature = trigger_build(project=project, version=version)[1] + build_pk = signature.get('kwargs', {}).get('build_pk') return HttpResponseRedirect( reverse('builds_detail', args=[project.slug, build_pk]) ) diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index 4c22e38dde0..848305b2908 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -165,7 +165,7 @@ def trigger_build(project, version=None, record=True, force=False): # Current project is skipped return None - return update_docs_task.apply_async() + return (update_docs_task.apply_async(), update_docs_task) def send_email(recipient, subject, template, template_html, context=None, From 51196b14f584b3c7d8afb8444d24220dc7a7af18 Mon Sep 17 00:00:00 2001 From: Rahul Tiwari Date: Mon, 1 Oct 2018 18:25:26 +0530 Subject: [PATCH 24/31] Update dosctring and fix yapf errors --- readthedocs/builds/views.py | 29 ++++++++--------------------- readthedocs/core/utils/__init__.py | 2 +- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index 8118d788d45..c44578b2863 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -35,13 +35,8 @@ class BuildBase(object): def get_queryset(self): self.project_slug = self.kwargs.get('project_slug', None) - self.project = get_object_or_404( - Project.objects.protected(self.request.user), - slug=self.project_slug, - ) - queryset = Build.objects.public( - user=self.request.user, project=self.project - ) + self.project = get_object_or_404(Project.objects.protected(self.request.user),slug=self.project_slug,) + queryset = Build.objects.public(user=self.request.user, project=self.project) return queryset @@ -62,11 +57,10 @@ def post(self, request, project_slug): slug=version_slug, ) - signature = trigger_build(project=project, version=version)[1] + _, signature = trigger_build(project=project, version=version) build_pk = signature.get('kwargs', {}).get('build_pk') return HttpResponseRedirect( - reverse('builds_detail', args=[project.slug, build_pk]) - ) + reverse('builds_detail', args=[project.slug, build_pk])) class BuildList(BuildBase, BuildTriggerMixin, ListView): @@ -74,14 +68,11 @@ class BuildList(BuildBase, BuildTriggerMixin, ListView): def get_context_data(self, **kwargs): context = super(BuildList, self).get_context_data(**kwargs) - active_builds = self.get_queryset().exclude(state='finished' - ).values('id') + active_builds = self.get_queryset().exclude(state='finished').values('id') context['project'] = self.project context['active_builds'] = active_builds - context['versions'] = Version.objects.public( - user=self.request.user, project=self.project - ) + context['versions'] = Version.objects.public(user=self.request.user, project=self.project) context['build_qs'] = self.get_queryset() return context @@ -100,12 +91,8 @@ def get_context_data(self, **kwargs): def builds_redirect_list(request, project_slug): # pylint: disable=unused-argument - return HttpResponsePermanentRedirect( - reverse('builds_project_list', args=[project_slug]) - ) + return HttpResponsePermanentRedirect(reverse('builds_project_list', args=[project_slug])) def builds_redirect_detail(request, project_slug, pk): # pylint: disable=unused-argument - return HttpResponsePermanentRedirect( - reverse('builds_detail', args=[project_slug, pk]) - ) + return HttpResponsePermanentRedirect(reverse('builds_detail', args=[project_slug, pk])) diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index 848305b2908..6393ed8eba5 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -151,7 +151,7 @@ def trigger_build(project, version=None, record=True, force=False): :param version: version of the project to be built. Default: ``latest`` :param record: whether or not record the build in a new Build object :param force: build the HTML documentation even if the files haven't changed - :returns: Celery AsyncResult promise + :returns: A tuple (Celery AsyncResult promise, Task Signature from ``prepare_build``) """ update_docs_task = prepare_build( project, From c972fb253d5c2cc88ed54c65545497a3638df4fb Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Fri, 2 Nov 2018 15:55:37 -0500 Subject: [PATCH 25/31] Add test for build redirect --- readthedocs/builds/views.py | 31 ++++-- readthedocs/core/utils/__init__.py | 49 +++++---- readthedocs/rtd_tests/tests/test_views.py | 126 +++++++++++++++------- 3 files changed, 143 insertions(+), 63 deletions(-) diff --git a/readthedocs/builds/views.py b/readthedocs/builds/views.py index c44578b2863..d8a9e7d45c5 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + """Views for builds app.""" from __future__ import ( @@ -35,8 +36,13 @@ class BuildBase(object): def get_queryset(self): self.project_slug = self.kwargs.get('project_slug', None) - self.project = get_object_or_404(Project.objects.protected(self.request.user),slug=self.project_slug,) - queryset = Build.objects.public(user=self.request.user, project=self.project) + self.project = get_object_or_404( + Project.objects.protected(self.request.user), + slug=self.project_slug, + ) + queryset = Build.objects.public( + user=self.request.user, project=self.project + ) return queryset @@ -57,10 +63,10 @@ def post(self, request, project_slug): slug=version_slug, ) - _, signature = trigger_build(project=project, version=version) - build_pk = signature.get('kwargs', {}).get('build_pk') + _, build = trigger_build(project=project, version=version) return HttpResponseRedirect( - reverse('builds_detail', args=[project.slug, build_pk])) + reverse('builds_detail', args=[project.slug, build.pk]), + ) class BuildList(BuildBase, BuildTriggerMixin, ListView): @@ -68,11 +74,14 @@ class BuildList(BuildBase, BuildTriggerMixin, ListView): def get_context_data(self, **kwargs): context = super(BuildList, self).get_context_data(**kwargs) - active_builds = self.get_queryset().exclude(state='finished').values('id') + active_builds = self.get_queryset().exclude(state='finished' + ).values('id') context['project'] = self.project context['active_builds'] = active_builds - context['versions'] = Version.objects.public(user=self.request.user, project=self.project) + context['versions'] = Version.objects.public( + user=self.request.user, project=self.project + ) context['build_qs'] = self.get_queryset() return context @@ -91,8 +100,12 @@ def get_context_data(self, **kwargs): def builds_redirect_list(request, project_slug): # pylint: disable=unused-argument - return HttpResponsePermanentRedirect(reverse('builds_project_list', args=[project_slug])) + return HttpResponsePermanentRedirect( + reverse('builds_project_list', args=[project_slug]) + ) def builds_redirect_detail(request, project_slug, pk): # pylint: disable=unused-argument - return HttpResponsePermanentRedirect(reverse('builds_detail', args=[project_slug, pk])) + return HttpResponsePermanentRedirect( + reverse('builds_detail', args=[project_slug, pk]) + ) diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index 6393ed8eba5..ea221c4149a 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + """Common utilty functions.""" from __future__ import absolute_import @@ -14,13 +15,11 @@ from django.utils.functional import allow_lazy from django.utils.safestring import SafeText, mark_safe from django.utils.text import slugify as slugify_base -from future.backports.urllib.parse import urlparse from celery import group, chord from readthedocs.builds.constants import LATEST, BUILD_STATE_TRIGGERED from readthedocs.doc_builder.constants import DOCKER_LIMITS - log = logging.getLogger(__name__) SYNC_USER = getattr(settings, 'SYNC_USER', getpass.getuser()) @@ -40,9 +39,9 @@ def broadcast(type, task, args, kwargs=None, callback=None): # pylint: disable= kwargs = {} default_queue = getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery') if type in ['web', 'app']: - servers = getattr(settings, "MULTIPLE_APP_SERVERS", [default_queue]) + servers = getattr(settings, 'MULTIPLE_APP_SERVERS', [default_queue]) elif type in ['build']: - servers = getattr(settings, "MULTIPLE_BUILD_SERVERS", [default_queue]) + servers = getattr(settings, 'MULTIPLE_BUILD_SERVERS', [default_queue]) tasks = [] for server in servers: @@ -71,7 +70,12 @@ def cname_to_slug(host): def prepare_build( - project, version=None, record=True, force=False, immutable=True): + project, + version=None, + record=True, + force=False, + immutable=True, +): """ Prepare a build in a Celery task for project and version. @@ -132,11 +136,14 @@ def prepare_build( options['soft_time_limit'] = time_limit options['time_limit'] = int(time_limit * 1.2) - return update_docs_task.signature( - args=(project.pk,), - kwargs=kwargs, - options=options, - immutable=True, + return ( + update_docs_task.signature( + args=(project.pk,), + kwargs=kwargs, + options=options, + immutable=True, + ), + build, ) @@ -153,7 +160,7 @@ def trigger_build(project, version=None, record=True, force=False): :param force: build the HTML documentation even if the files haven't changed :returns: A tuple (Celery AsyncResult promise, Task Signature from ``prepare_build``) """ - update_docs_task = prepare_build( + update_docs_task, build = prepare_build( project, version, record, @@ -165,11 +172,13 @@ def trigger_build(project, version=None, record=True, force=False): # Current project is skipped return None - return (update_docs_task.apply_async(), update_docs_task) + return (update_docs_task.apply_async(), build) -def send_email(recipient, subject, template, template_html, context=None, - request=None, from_email=None, **kwargs): # pylint: disable=unused-argument +def send_email( + recipient, subject, template, template_html, context=None, request=None, + from_email=None, **kwargs +): # pylint: disable=unused-argument """ Alter context passed in and call email send task. @@ -183,10 +192,14 @@ def send_email(recipient, subject, template, template_html, context=None, if context is None: context = {} context['uri'] = '{scheme}://{host}'.format( - scheme='https', host=settings.PRODUCTION_DOMAIN) - send_email_task.delay(recipient=recipient, subject=subject, template=template, - template_html=template_html, context=context, from_email=from_email, - **kwargs) + scheme='https', + host=settings.PRODUCTION_DOMAIN, + ) + send_email_task.delay( + recipient=recipient, subject=subject, template=template, + template_html=template_html, context=context, from_email=from_email, + **kwargs + ) def slugify(value, *args, **kwargs): diff --git a/readthedocs/rtd_tests/tests/test_views.py b/readthedocs/rtd_tests/tests/test_views.py index c1bcf92303e..cfe90e1dc4c 100644 --- a/readthedocs/rtd_tests/tests/test_views.py +++ b/readthedocs/rtd_tests/tests/test_views.py @@ -1,19 +1,26 @@ -from __future__ import absolute_import +# -*- coding: utf-8 -*- +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +import mock from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test import TestCase from django.utils.six.moves.urllib.parse import urlsplit -from django_dynamic_fixture import get -from django_dynamic_fixture import new +from django_dynamic_fixture import get, new from readthedocs.builds.constants import LATEST +from readthedocs.builds.models import Build from readthedocs.core.permissions import AdminPermission -from readthedocs.projects.models import ImportedFile -from readthedocs.projects.models import Project from readthedocs.projects.forms import UpdateProjectForm - +from readthedocs.projects.models import ImportedFile, Project class Testmaker(TestCase): + def setUp(self): self.eric = User(username='eric') self.eric.set_password('test') @@ -27,21 +34,24 @@ def test_imported_docs(self): self.assertEqual(r.status_code, 200) r = self.client.get('/dashboard/import/manual/', {}) self.assertEqual(r.status_code, 200) - form = UpdateProjectForm(data={ - 'name': 'Django Kong', - 'repo': 'https://github.com/ericholscher/django-kong', - 'repo_type': 'git', - 'description': 'OOHHH AH AH AH KONG SMASH', - 'language': 'en', - 'default_branch': '', - 'project_url': 'http://django-kong.rtfd.org', - 'default_version': LATEST, - 'privacy_level': 'public', - 'version_privacy_level': 'public', - 'python_interpreter': 'python', - 'documentation_type': 'sphinx', - 'csrfmiddlewaretoken': '34af7c8a5ba84b84564403a280d9a9be', - }, user=user) + form = UpdateProjectForm( + data={ + 'name': 'Django Kong', + 'repo': 'https://github.com/ericholscher/django-kong', + 'repo_type': 'git', + 'description': 'OOHHH AH AH AH KONG SMASH', + 'language': 'en', + 'default_branch': '', + 'project_url': 'http://django-kong.rtfd.org', + 'default_version': LATEST, + 'privacy_level': 'public', + 'version_privacy_level': 'public', + 'python_interpreter': 'python', + 'documentation_type': 'sphinx', + 'csrfmiddlewaretoken': '34af7c8a5ba84b84564403a280d9a9be', + }, + user=user, + ) _ = form.save() _ = Project.objects.get(slug='django-kong') @@ -111,11 +121,13 @@ def test_project_delete(self): def test_subprojects_delete(self): # This URL doesn't exist anymore, 404 response = self.client.get( - '/dashboard/pip/subprojects/delete/a-subproject/') + '/dashboard/pip/subprojects/delete/a-subproject/', + ) self.assertEqual(response.status_code, 404) # New URL response = self.client.get( - '/dashboard/pip/subprojects/a-subproject/delete/') + '/dashboard/pip/subprojects/a-subproject/delete/', + ) self.assertRedirectToLogin(response) def test_subprojects(self): @@ -143,7 +155,9 @@ def test_project_translations(self): self.assertRedirectToLogin(response) def test_project_translations_delete(self): - response = self.client.get('/dashboard/pip/translations/delete/a-translation/') + response = self.client.get( + '/dashboard/pip/translations/delete/a-translation/' + ) self.assertRedirectToLogin(response) def test_project_redirects(self): @@ -168,7 +182,8 @@ def setUp(self): slug='file', path='file.html', md5='abcdef', - commit='1234567890abcdef') + commit='1234567890abcdef', + ) def test_random_page_view_redirects(self): response = self.client.get('/random/') @@ -188,7 +203,9 @@ def test_404_for_with_no_imported_files(self): response = self.client.get('/random/pip/') self.assertEqual(response.status_code, 404) + class SubprojectViewTests(TestCase): + def setUp(self): self.user = new(User, username='test') self.user.set_password('test') @@ -201,10 +218,15 @@ def setUp(self): self.client.login(username='test', password='test') def test_deny_delete_for_non_project_admins(self): - response = self.client.get('/dashboard/my-mainproject/subprojects/delete/my-subproject/') + response = self.client.get( + '/dashboard/my-mainproject/subprojects/delete/my-subproject/' + ) self.assertEqual(response.status_code, 404) - self.assertTrue(self.subproject in [r.child for r in self.project.subprojects.all()]) + self.assertTrue( + self.subproject in + [r.child for r in self.project.subprojects.all()] + ) def test_admins_can_delete_subprojects(self): self.project.users.add(self.user) @@ -212,24 +234,56 @@ def test_admins_can_delete_subprojects(self): # URL doesn't exist anymore, 404 response = self.client.get( - '/dashboard/my-mainproject/subprojects/delete/my-subproject/') + '/dashboard/my-mainproject/subprojects/delete/my-subproject/', + ) self.assertEqual(response.status_code, 404) # This URL still doesn't accept GET, 405 response = self.client.get( - '/dashboard/my-mainproject/subprojects/my-subproject/delete/') + '/dashboard/my-mainproject/subprojects/my-subproject/delete/', + ) self.assertEqual(response.status_code, 405) - self.assertTrue(self.subproject in [r.child for r in self.project.subprojects.all()]) + self.assertTrue( + self.subproject in + [r.child for r in self.project.subprojects.all()] + ) # Test POST response = self.client.post( - '/dashboard/my-mainproject/subprojects/my-subproject/delete/') + '/dashboard/my-mainproject/subprojects/my-subproject/delete/', + ) self.assertEqual(response.status_code, 302) - self.assertTrue(self.subproject not in [r.child for r in self.project.subprojects.all()]) - - def test_project_admins_can_delete_subprojects_that_they_are_not_admin_of(self): + self.assertTrue( + self.subproject not in + [r.child for r in self.project.subprojects.all()] + ) + + def test_project_admins_can_delete_subprojects_that_they_are_not_admin_of( + self + ): self.project.users.add(self.user) self.assertFalse(AdminPermission.is_admin(self.user, self.subproject)) response = self.client.post( - '/dashboard/my-mainproject/subprojects/my-subproject/delete/') + '/dashboard/my-mainproject/subprojects/my-subproject/delete/', + ) self.assertEqual(response.status_code, 302) - self.assertTrue(self.subproject not in [r.child for r in self.project.subprojects.all()]) + self.assertTrue( + self.subproject not in + [r.child for r in self.project.subprojects.all()] + ) + + +class BuildViewTests(TestCase): + fixtures = ['eric', 'test_data'] + + def setUp(self): + self.client.login(username='eric', password='test') + + @mock.patch('readthedocs.projects.tasks.update_docs_task') + def test_build_redirect(self, mock): + r = self.client.post('/projects/pip/builds/', {'version_slug': '0.8.1'}) + build = Build.objects.filter(project__slug='pip').latest() + self.assertEqual(r.status_code, 302) + self.assertEqual( + r._headers['location'][1], + '/projects/pip/builds/%s/' % build.pk, + ) From fc75ac371cef5e46f96e9b6203c15de77afe41c6 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Fri, 2 Nov 2018 16:11:45 -0500 Subject: [PATCH 26/31] Fix rtd config file --- .readthedocs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 3debbab6f59..0e55d253be9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,5 +3,4 @@ formats: all sphinx: configuration: docs/conf.py python: - install: - requirements: requirements.txt + requirements: requirements.txt From de1223bb845e4654e7dc66b2d6a4e70626e2c8be Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Fri, 2 Nov 2018 16:14:06 -0500 Subject: [PATCH 27/31] Fix usage of prepare_build --- readthedocs/projects/views/private.py | 203 +++++++++++++++++++------- 1 file changed, 147 insertions(+), 56 deletions(-) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 897388c14ba..9b3092e4a30 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- + """Project views for authenticated users.""" from __future__ import ( - absolute_import, division, print_function, unicode_literals) + absolute_import, + division, + print_function, + unicode_literals, +) import logging @@ -13,8 +18,11 @@ from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.http import ( - Http404, HttpResponseBadRequest, HttpResponseNotAllowed, - HttpResponseRedirect) + Http404, + HttpResponseBadRequest, + HttpResponseNotAllowed, + HttpResponseRedirect, +) from django.middleware.csrf import get_token from django.shortcuts import get_object_or_404, render from django.utils.safestring import mark_safe @@ -22,22 +30,39 @@ from django.views.generic import ListView, TemplateView, View from formtools.wizard.views import SessionWizardView from vanilla import CreateView, DeleteView, DetailView, GenericView, UpdateView + from readthedocs.builds.forms import VersionForm from readthedocs.builds.models import Version from readthedocs.core.mixins import ListViewWithForm, LoginRequiredMixin -from readthedocs.core.utils import broadcast, trigger_build, prepare_build +from readthedocs.core.utils import broadcast, prepare_build, trigger_build from readthedocs.integrations.models import HttpExchange, Integration from readthedocs.oauth.services import registry -from readthedocs.oauth.utils import update_webhook from readthedocs.oauth.tasks import attach_webhook +from readthedocs.oauth.utils import update_webhook from readthedocs.projects import tasks from readthedocs.projects.forms import ( - DomainForm, EmailHookForm, IntegrationForm, ProjectAdvancedForm, - ProjectAdvertisingForm, ProjectBasicsForm, ProjectExtraForm, - ProjectRelationshipForm, RedirectForm, TranslationForm, UpdateProjectForm, - UserForm, WebHookForm, build_versions_form) + DomainForm, + EmailHookForm, + IntegrationForm, + ProjectAdvancedForm, + ProjectAdvertisingForm, + ProjectBasicsForm, + ProjectExtraForm, + ProjectRelationshipForm, + RedirectForm, + TranslationForm, + UpdateProjectForm, + UserForm, + WebHookForm, + build_versions_form, +) from readthedocs.projects.models import ( - Domain, EmailHook, Project, ProjectRelationship, WebHook) + Domain, + EmailHook, + Project, + ProjectRelationship, + WebHook, +) from readthedocs.projects.signals import project_import from readthedocs.projects.views.base import ProjectAdminMixin, ProjectSpamMixin @@ -119,7 +144,9 @@ def project_versions(request, project_slug): like to have built. """ project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) if not project.is_imported: raise Http404 @@ -135,19 +162,27 @@ def project_versions(request, project_slug): return HttpResponseRedirect(project_dashboard) return render( - request, 'projects/project_versions.html', - {'form': form, 'project': project}) + request, + 'projects/project_versions.html', + {'form': form, 'project': project}, + ) @login_required def project_version_detail(request, project_slug, version_slug): """Project version detail page.""" project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) version = get_object_or_404( Version.objects.public( - user=request.user, project=project, only_active=False), - slug=version_slug) + user=request.user, + project=project, + only_active=False, + ), + slug=version_slug, + ) form = VersionForm(request.POST or None, instance=version) @@ -157,15 +192,20 @@ def project_version_detail(request, project_slug, version_slug): if 'active' in form.changed_data and version.active is False: log.info('Removing files for version %s', version.slug) broadcast( - type='app', task=tasks.clear_artifacts, args=[version.get_artifact_paths()]) + type='app', + task=tasks.clear_artifacts, + args=[version.get_artifact_paths()], + ) version.built = False version.save() url = reverse('project_version_list', args=[project.slug]) return HttpResponseRedirect(url) return render( - request, 'projects/project_version_detail.html', - {'form': form, 'project': project, 'version': version}) + request, + 'projects/project_version_detail.html', + {'form': form, 'project': project, 'version': version}, + ) @login_required @@ -177,7 +217,9 @@ def project_delete(request, project_slug): confirmation of delete. """ project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) if request.method == 'POST': broadcast(type='app', task=tasks.remove_dir, args=[project.doc_path]) @@ -240,11 +282,12 @@ def done(self, form_list, **kwargs): self.trigger_initial_build(project) return HttpResponseRedirect( - reverse('projects_detail', args=[project.slug])) + reverse('projects_detail', args=[project.slug]), + ) def trigger_initial_build(self, project): """Trigger initial build.""" - update_docs = prepare_build(project) + update_docs, build = prepare_build(project) task_promise = chain( attach_webhook.si(project.pk, self.request.user.pk), update_docs, @@ -275,10 +318,13 @@ def get(self, request, *args, **kwargs): data = self.get_form_data() project = Project.objects.for_admin_user( - request.user).filter(repo=data['repo']).first() + request.user, + ).filter(repo=data['repo']).first() if project is not None: messages.success( - request, _('The demo project is already imported!')) + request, + _('The demo project is already imported!'), + ) else: kwargs = self.get_form_kwargs() form = self.form_class(data=data, **kwargs) @@ -287,7 +333,9 @@ def get(self, request, *args, **kwargs): project.save() trigger_build(project) messages.success( - request, _('Your demo project is currently being imported')) + request, + _('Your demo project is currently being imported'), + ) else: messages.error( request, @@ -295,14 +343,15 @@ def get(self, request, *args, **kwargs): ) return HttpResponseRedirect(reverse('projects_dashboard')) return HttpResponseRedirect( - reverse('projects_detail', args=[project.slug])) + reverse('projects_detail', args=[project.slug]), + ) def get_form_data(self): """Get form data to post to import form.""" return { 'name': '{0}-demo'.format(self.request.user.username), 'repo_type': 'git', - 'repo': 'https://github.com/readthedocs/template.git' + 'repo': 'https://github.com/readthedocs/template.git', } def get_form_kwargs(self): @@ -336,7 +385,8 @@ def get(self, request, *args, **kwargs): .exclude( provider__in=[ service.adapter.provider_id for service in registry - ]) + ], + ) ) # yapf: disable for account in deprecated_accounts: provider_account = account.get_provider_account() @@ -346,10 +396,12 @@ def get(self, request, *args, **kwargs): _( 'There is a problem with your {service} account, ' 'try reconnecting your account on your ' - 'connected services page.').format( - service=provider_account.get_brand()['name'], - url=reverse('socialaccount_connections')) - )) # yapf: disable + 'connected services page.', + ).format( + service=provider_account.get_brand()['name'], + url=reverse('socialaccount_connections'), + ) + )), # yapf: disable ) return super(ImportView, self).get(request, *args, **kwargs) @@ -368,7 +420,8 @@ def get_context_data(self, **kwargs): context = super(ImportView, self).get_context_data(**kwargs) context['view_csrf_token'] = get_token(self.request) context['has_connected_accounts'] = SocialAccount.objects.filter( - user=self.request.user).exists() + user=self.request.user, + ).exists() return context @@ -385,8 +438,10 @@ def get_queryset(self): def get_form(self, data=None, files=None, **kwargs): kwargs['user'] = self.request.user - return super(ProjectRelationshipMixin, - self).get_form(data, files, **kwargs) + return super( + ProjectRelationshipMixin, + self, + ).get_form(data, files, **kwargs) def form_valid(self, form): broadcast( @@ -426,7 +481,9 @@ def get(self, request, *args, **kwargs): def project_users(request, project_slug): """Project users view and form view.""" project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) form = UserForm(data=request.POST or None, project=project) @@ -449,9 +506,13 @@ def project_users_delete(request, project_slug): if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) user = get_object_or_404( - User.objects.all(), username=request.POST.get('username')) + User.objects.all(), + username=request.POST.get('username'), + ) if user == request.user: raise Http404 project.users.remove(user) @@ -463,7 +524,9 @@ def project_users_delete(request, project_slug): def project_notifications(request, project_slug): """Project notification view and form view.""" project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) email_form = EmailHookForm(data=request.POST or None, project=project) webhook_form = WebHookForm(data=request.POST or None, project=project) @@ -501,14 +564,18 @@ def project_notifications_delete(request, project_slug): if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) try: project.emailhook_notifications.get( - email=request.POST.get('email')).delete() + email=request.POST.get('email'), + ).delete() except EmailHook.DoesNotExist: try: project.webhook_notifications.get( - url=request.POST.get('email')).delete() + url=request.POST.get('email'), + ).delete() except WebHook.DoesNotExist: raise Http404 project_dashboard = reverse('projects_notifications', args=[project.slug]) @@ -519,7 +586,9 @@ def project_notifications_delete(request, project_slug): def project_translations(request, project_slug): """Project translations view and form view.""" project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) form = TranslationForm( data=request.POST or None, parent=project, @@ -566,7 +635,9 @@ def project_translations_delete(request, project_slug, child_slug): def project_redirects(request, project_slug): """Project redirects view and form view.""" project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) form = RedirectForm(data=request.POST or None, project=project) @@ -578,8 +649,10 @@ def project_redirects(request, project_slug): redirects = project.redirects.all() return render( - request, 'projects/project_redirects.html', - {'form': form, 'project': project, 'redirects': redirects}) + request, + 'projects/project_redirects.html', + {'form': form, 'project': project, 'redirects': redirects}, + ) @login_required @@ -588,15 +661,20 @@ def project_redirects_delete(request, project_slug): if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) redirect = get_object_or_404( - project.redirects, pk=request.POST.get('id_pk')) + project.redirects, + pk=request.POST.get('id_pk'), + ) if redirect.project == project: redirect.delete() else: raise Http404 return HttpResponseRedirect( - reverse('projects_redirects', args=[project.slug])) + reverse('projects_redirects', args=[project.slug]), + ) @login_required @@ -607,21 +685,33 @@ def project_version_delete_html(request, project_slug, version_slug): This marks a version as not built """ project = get_object_or_404( - Project.objects.for_admin_user(request.user), slug=project_slug) + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) version = get_object_or_404( Version.objects.public( - user=request.user, project=project, only_active=False), - slug=version_slug) + user=request.user, + project=project, + only_active=False, + ), + slug=version_slug, + ) if not version.active: version.built = False version.save() - broadcast(type='app', task=tasks.clear_artifacts, args=[version.get_artifact_paths()]) + broadcast( + type='app', + task=tasks.clear_artifacts, + args=[version.get_artifact_paths()], + ) else: return HttpResponseBadRequest( - "Can't delete HTML for an active version.") + "Can't delete HTML for an active version.", + ) return HttpResponseRedirect( - reverse('project_version_list', kwargs={'project_slug': project_slug})) + reverse('project_version_list', kwargs={'project_slug': project_slug}), + ) class DomainMixin(ProjectAdminMixin, PrivateViewMixin): @@ -719,7 +809,8 @@ def get_template_names(self): suffix = self.SUFFIX_MAP.get(integration_type, integration_type) return ( 'projects/integration_{0}{1}.html' - .format(suffix, self.template_name_suffix)) + .format(suffix, self.template_name_suffix) + ) class IntegrationDelete(IntegrationMixin, DeleteView): From b6924f404cf698d87b6351496b6f5556334941da Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Sun, 4 Nov 2018 12:22:31 +0530 Subject: [PATCH 28/31] Change 'VerisionLockedTimeout' to 'VersionLockedError' in comment --- readthedocs/projects/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index ccc6e00b050..2c3922e4505 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -420,7 +420,7 @@ def run_setup(self, record=True): )) # Send notification to users only if the build didn't fail because - # of VerisionLockedTimeout: this exception occurs when a build is + # of VersionLockedError: this exception occurs when a build is # triggered before the previous one has finished (e.g. two webhooks, # one after the other) if not isinstance(self.setup_env.failure, VersionLockedError): From c4f8212d50e0da03d209a4c65a23bce953373070 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Mon, 5 Nov 2018 12:39:14 -0600 Subject: [PATCH 29/31] Fix migration name on modified date migration --- ...e_importedfile.py => 0031_add_modified_date_importedfile.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename readthedocs/projects/migrations/{0030_add_modified_date_importedfile.py => 0031_add_modified_date_importedfile.py} (88%) diff --git a/readthedocs/projects/migrations/0030_add_modified_date_importedfile.py b/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py similarity index 88% rename from readthedocs/projects/migrations/0030_add_modified_date_importedfile.py rename to readthedocs/projects/migrations/0031_add_modified_date_importedfile.py index 3ad50c93bc7..60c3f0fe33d 100644 --- a/readthedocs/projects/migrations/0030_add_modified_date_importedfile.py +++ b/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('projects', '0029_add_additional_languages'), + ('projects', '0030_add_modified_date_importedfile'), ] operations = [ From 8430b6be89b3989fdfa498347333707e440196b3 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Mon, 5 Nov 2018 12:55:39 -0600 Subject: [PATCH 30/31] Fix migration dependency --- .../projects/migrations/0031_add_modified_date_importedfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py b/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py index 60c3f0fe33d..255da1c003a 100644 --- a/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py +++ b/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('projects', '0030_add_modified_date_importedfile'), + ('projects', '0030_change-max-length-project-slug'), ] operations = [ From fc754e1bc2e6ee5aa41e504da83c993ccd001c21 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 6 Nov 2018 10:50:46 -0600 Subject: [PATCH 31/31] Release 2.8.1 --- CHANGELOG.rst | 58 +++++++++++++++++++ .../static/core/js/readthedocs-doc-embed.js | 2 +- .../projects/static/projects/js/tools.js | 2 +- setup.cfg | 2 +- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index deae5c420dc..49af8daf309 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,61 @@ +Version 2.8.1 +------------- + +:Date: November 06, 2018 + +* `@ericholscher `__: Fix migration name on modified date migration (`#4867 `__) +* `@dojutsu-user `__: Change 'VerisionLockedTimeout' to 'VersionLockedError' in comment. (`#4859 `__) +* `@stsewd `__: Fix rtd config file (`#4857 `__) +* `@ericholscher `__: Shorten project name to match slug length (`#4856 `__) +* `@stsewd `__: Generic message for parser error of config file (`#4853 `__) +* `@stsewd `__: Use $HOME as CWD for virtualenv creation (`#4852 `__) +* `@stsewd `__: Hide "edit on" when the version is a tag (`#4851 `__) +* `@ericholscher `__: Add modified_date to ImportedFile. (`#4850 `__) +* `@ericholscher `__: Use raw_id_fields so that the Feature admin loads (`#4849 `__) +* `@stsewd `__: Allow to change project's VCS (`#4845 `__) +* `@benjaoming `__: Version compare warning text (`#4842 `__) +* `@dojutsu-user `__: Make form for adopting project a choice field (`#4841 `__) +* `@humitos `__: Do not send notification on VersionLockedError (`#4839 `__) +* `@stsewd `__: Start testing config v2 on our project (`#4838 `__) +* `@ericholscher `__: Add all migrations that are missing from model changes (`#4837 `__) +* `@ericholscher `__: Add docstring to DrfJsonSerializer so we know why it's there (`#4836 `__) +* `@ericholscher `__: Show the project's slug in the dashboard (`#4834 `__) +* `@humitos `__: Avoid infinite redirection (`#4833 `__) +* `@ericholscher `__: Allow filtering builds by commit. (`#4831 `__) +* `@dojutsu-user `__: Add 'Branding' under the 'Business Info' section and 'Guidelines' on 'Design Docs' (`#4830 `__) +* `@davidfischer `__: Migrate old passwords without "set_unusable_password" (`#4829 `__) +* `@humitos `__: Do not import the Celery worker when running the Django app (`#4824 `__) +* `@damianz5 `__: Fix for jQuery in doc-embed call (`#4819 `__) +* `@invinciblycool `__: Add MkDocsYAMLParseError (`#4814 `__) +* `@stsewd `__: Delete untracked tags on fetch (`#4811 `__) +* `@stsewd `__: Don't activate version on build (`#4810 `__) +* `@humitos `__: Feature flag to make `readthedocs` theme default on MkDocs docs (`#4802 `__) +* `@ericholscher `__: Allow use of `file://` urls in repos during development. (`#4801 `__) +* `@ericholscher `__: Release 2.7.2 (`#4796 `__) +* `@dojutsu-user `__: Raise 404 at SubdomainMiddleware if the project does not exist. (`#4795 `__) +* `@dojutsu-user `__: Add help_text in the form for adopting a project (`#4781 `__) +* `@humitos `__: Add VAT ID field for Gold User (`#4776 `__) +* `@sriks123 `__: Remove logic around finding config file inside directories (`#4755 `__) +* `@dojutsu-user `__: Improve unexpected error message when build fails (`#4754 `__) +* `@stsewd `__: Don't build latest on webhook if it is deactivated (`#4733 `__) +* `@dojutsu-user `__: Change the way of using login_required decorator (`#4723 `__) +* `@invinciblycool `__: Remove unused views and their translations. (`#4632 `__) +* `@invinciblycool `__: Redirect to build detail post manual build (`#4622 `__) +* `@anubhavsinha98 `__: Issue #4551 Changed mock docks to use sphinx (`#4569 `__) +* `@xrmx `__: search: mark more strings for translation (`#4438 `__) +* `@Alig1493 `__: Fix for issue #4092: Remove unused field from Project model (`#4431 `__) +* `@mashrikt `__: Remove pytest _describe (`#4429 `__) +* `@xrmx `__: static: use modern getJSON callbacks (`#4382 `__) +* `@jaraco `__: Script for creating a project (`#4370 `__) +* `@xrmx `__: make it easier to use a different default theme (`#4278 `__) +* `@humitos `__: Document alternate domains for business site (`#4271 `__) +* `@xrmx `__: restapi/client: don't use DRF parser for parsing (`#4160 `__) +* `@julienmalard `__: New languages (`#3759 `__) +* `@stsewd `__: Improve installation guide (`#3631 `__) +* `@stsewd `__: Allow to hide version warning (`#3595 `__) +* `@Alig1493 `__: [Fixed #872] Filter Builds according to commit (`#3544 `__) +* `@stsewd `__: Make slug field a valid DNS label (`#3464 `__) + Version 2.8.0 ------------- diff --git a/readthedocs/core/static/core/js/readthedocs-doc-embed.js b/readthedocs/core/static/core/js/readthedocs-doc-embed.js index bdd9a1ec789..1e18228d393 100644 --- a/readthedocs/core/static/core/js/readthedocs-doc-embed.js +++ b/readthedocs/core/static/core/js/readthedocs-doc-embed.js @@ -1 +1 @@ -!function o(s,a,l){function d(t,e){if(!a[t]){if(!s[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(c)return c(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var n=a[t]={exports:{}};s[t][0].call(n.exports,function(e){return d(s[t][1][e]||e)},n,n.exports,o,s,a,l)}return a[t].exports}for(var c="function"==typeof require&&require,e=0;e"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var t=i(this);expand=i(''),expand.on("click",function(e){return r.toggleCurrent(t),e.stopPropagation(),!1}),t.prepend(expand)})},reset:function(){var e=encodeURI(window.location.hash)||"#";try{var t=$(".wy-menu-vertical"),i=t.find('[href="'+e+'"]');if(0===i.length){var r=$('.document [id="'+e.substring(1)+'"]').closest("div.section");0===(i=t.find('[href="#'+r.attr("id")+'"]')).length&&(i=t.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=e)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(e){var t=e.closest("li");t.siblings("li.current").removeClass("current"),t.siblings().find("li.current").removeClass("current"),t.find("> ul li.current").removeClass("current"),t.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:t.exports.ThemeNav,StickyNav:t.exports.ThemeNav}),function(){for(var o=0,e=["ms","moz","webkit","o"],t=0;t/g,c=/"/g,p=/"/g,h=/&#([a-zA-Z0-9]*);?/gim,f=/:?/gim,m=/&newline;?/gim,g=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi,v=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi,b=/u\s*r\s*l\s*\(.*/gi;function w(e){return e.replace(c,""")}function y(e){return e.replace(p,'"')}function _(e){return e.replace(h,function(e,t){return"x"===t[0]||"X"===t[0]?String.fromCharCode(parseInt(t.substr(1),16)):String.fromCharCode(parseInt(t,10))})}function x(e){return e.replace(f,":").replace(m," ")}function k(e){for(var t="",i=0,r=e.length;i/g;i.whiteList={a:["target","href","title"],abbr:["title"],address:[],area:["shape","coords","href","alt"],article:[],aside:[],audio:["autoplay","controls","loop","preload","src"],b:[],bdi:["dir"],bdo:["dir"],big:[],blockquote:["cite"],br:[],caption:[],center:[],cite:[],code:[],col:["align","valign","span","width"],colgroup:["align","valign","span","width"],dd:[],del:["datetime"],details:["open"],div:[],dl:[],dt:[],em:[],font:["color","size","face"],footer:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],header:[],hr:[],i:[],img:["src","alt","title","width","height"],ins:["datetime"],li:[],mark:[],nav:[],ol:[],p:[],pre:[],s:[],section:[],small:[],span:[],sub:[],sup:[],strong:[],table:["width","border","align","valign"],tbody:["align","valign"],td:["width","rowspan","colspan","align","valign"],tfoot:["align","valign"],th:["width","rowspan","colspan","align","valign"],thead:["align","valign"],tr:["rowspan","align","valign"],tt:[],u:[],ul:[],video:["autoplay","controls","loop","preload","src","height","width"]},i.getDefaultWhiteList=o,i.onTag=function(e,t,i){},i.onIgnoreTag=function(e,t,i){},i.onTagAttr=function(e,t,i){},i.onIgnoreTagAttr=function(e,t,i){},i.safeAttrValue=function(e,t,i,r){if(i=T(i),"href"===t||"src"===t){if("#"===(i=u.trim(i)))return"#";if("http://"!==i.substr(0,7)&&"https://"!==i.substr(0,8)&&"mailto:"!==i.substr(0,7)&&"tel:"!==i.substr(0,4)&&"#"!==i[0]&&"/"!==i[0])return""}else if("background"===t){if(g.lastIndex=0,g.test(i))return""}else if("style"===t){if(v.lastIndex=0,v.test(i))return"";if(b.lastIndex=0,b.test(i)&&(g.lastIndex=0,g.test(i)))return"";!1!==r&&(i=(r=r||s).process(i))}return i=E(i)},i.escapeHtml=a,i.escapeQuote=w,i.unescapeQuote=y,i.escapeHtmlEntities=_,i.escapeDangerHtml5Entities=x,i.clearNonPrintableCharacter=k,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(s,a){"function"!=typeof a&&(a=function(){});var l=!Array.isArray(s),d=[],c=!1;return{onIgnoreTag:function(e,t,i){if(o=e,l||-1!==u.indexOf(s,o)){if(i.isClosing){var r="[/removed]",n=i.position+r.length;return d.push([!1!==c?c:i.position,n]),c=!1,r}return c||(c=i.position),"[removed]"}return a(e,t,i);var o},remove:function(t){var i="",r=0;return u.forEach(d,function(e){i+=t.slice(r,e[0]),r=e[1]}),i+=t.slice(r)}}},i.stripCommentTag=function(e){return e.replace(A,"")},i.stripBlankChar=function(e){var t=e.split("");return(t=t.filter(function(e){var t=e.charCodeAt(0);return!(127===t||t<=31&&10!==t&&13!==t)})).join("")},i.cssFilter=s,i.getDefaultCSSWhiteList=n},{"./util":5,cssfilter:10}],3:[function(e,t,i){var r=e("./default"),n=e("./parser"),o=e("./xss");for(var s in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,r)i[s]=r[s];for(var s in n)i[s]=n[s];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var c=e("./util");function p(e){var t=c.spaceIndex(e);if(-1===t)var i=e.slice(1,-1);else i=e.slice(1,t+1);return"/"===(i=c.trim(i).toLowerCase()).slice(0,1)&&(i=i.slice(1)),"/"===i.slice(-1)&&(i=i.slice(0,-1)),i}var u=/[^a-zA-Z0-9_:\.\-]/gim;function h(e,t){for(;t"===u){r+=i(e.slice(n,o)),c=p(d=e.slice(o,a+1)),r+=t(o,r.length,c,d,"";var a=function(e){var t=w.spaceIndex(e);if(-1===t)return{html:"",closing:"/"===e[e.length-2]};var i="/"===(e=w.trim(e.slice(t+1,-1)))[e.length-1];return i&&(e=w.trim(e.slice(0,-1))),{html:e,closing:i}}(i),l=c[n],d=b(a.html,function(e,t){var i,r=-1!==w.indexOf(l,e);return y(i=h(n,e,t,r))?r?(t=m(n,e,t,v))?e+'="'+t+'"':e:y(i=f(n,e,t,r))?void 0:i:i});i="<"+n;return d&&(i+=" "+d),a.closing&&(i+=" /"),i+=">"}return y(o=p(n,i,s))?g(i):o},g);return i&&(r=i.remove(r)),r},t.exports=a},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var r,n;r=this,n=function(){var T=!0;function s(i){function e(e){var t=i.match(e);return t&&1t[1][i])return 1;if(t[0][i]!==t[1][i])return-1;if(0===i)return 0}}function o(e,t,i){var r=a;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(r=s(i));var n=""+r.version;for(var o in e)if(e.hasOwnProperty(o)&&r[o]){if("string"!=typeof e[o])throw new Error("Browser version in the minVersion map should be a string: "+o+": "+String(e));return E([n,e[o]])<0}return t}return a.test=function(e){for(var t=0;t'),s=document.createElement("a"),a=r.highlight;if(s.href+=n.link+DOCUMENTATION_OPTIONS.FILE_SUFFIX,s.search="?highlight="+$.urlencode(d),o.append($("").attr("href",s).html(n.title)),-1===n.project.indexOf(c)&&o.append($("").text(" (from project "+n.project+")")),a.content.length){var l=$('
').html(u(a.content[0]));l.find("em").addClass("highlighted"),o.append(l)}Search.output.append(o),o.slideDown(5)}t.length?Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length)):Search.query_fallback(d)}).fail(function(e){Search.query_fallback(d)}).always(function(){$("#search-progress").empty(),Search.stopPulse(),Search.title.text(_("Search Results")),Search.status.fadeIn(500)}),$.ajax({url:e.href,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){return void 0===e.responseJSON||void 0===e.responseJSON.results?r.reject():r.resolve(e.responseJSON.results)}}).fail(function(e,t,i){return r.reject()})}}$(document).ready(function(){"undefined"!=typeof Search&&Search.init()})}(r.get())}}},{"./../../../../../../bower_components/xss/lib/index":3,"./rtd-data":15}],17:[function(n,e,t){var o=n("./rtd-data");e.exports={init:function(){var e=o.get();if($(document).on("click","[data-toggle='rst-current-version']",function(){var e=$("[data-toggle='rst-versions']").hasClass("shift-up")?"was_open":"was_closed";"undefined"!=typeof ga?ga("rtfd.send","event","Flyout","Click",e):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Flyout","Click",e])}),void 0===window.SphinxRtdTheme){var t=n("./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js").ThemeNav;if($(document).ready(function(){setTimeout(function(){t.navBar||t.enable()},1e3)}),e.is_rtd_like_theme()&&!$("div.wy-side-scroll:first").length){console.log("Applying theme sidebar fix...");var i=$("nav.wy-nav-side:first"),r=$("
").addClass("wy-side-scroll");i.children().detach().appendTo(r),r.prependTo(i),t.navBar=r}}}}},{"./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js":1,"./rtd-data":15}],18:[function(e,t,i){var u,p=e("./constants"),h=e("./rtd-data"),r=e("bowser"),f="#ethical-ad-placement";function m(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),r=p.PROMO_TYPES.LEFTNAV,n=p.DEFAULT_PROMO_PRIORITY,o=null;return u.is_mkdocs_builder()&&u.is_rtd_like_theme()?(o="nav.wy-nav-side",e="ethical-rtd ethical-dark-theme"):u.is_rtd_like_theme()?(o="nav.wy-nav-side > div.wy-side-scroll",e="ethical-rtd ethical-dark-theme"):u.is_alabaster_like_theme()&&(o="div.sphinxsidebar > div.sphinxsidebarwrapper",e="ethical-alabaster"),o?($("
").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top>$(window).height())&&(n=p.LOW_PROMO_PRIORITY),{div_id:i,display_type:r,priority:n}):null}function g(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),r=p.PROMO_TYPES.FOOTER,n=p.DEFAULT_PROMO_PRIORITY,o=null;return u.is_rtd_like_theme()?(o=$("
").insertAfter("footer hr"),e="ethical-rtd"):u.is_alabaster_like_theme()&&(o="div.bodywrapper .body",e="ethical-alabaster"),o?($("
").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top<$(window).height())&&(n=p.LOW_PROMO_PRIORITY),{div_id:i,display_type:r,priority:n}):null}function v(){var e="rtd-"+(Math.random()+1).toString(36).substring(4),t=p.PROMO_TYPES.FIXED_FOOTER;return r&&r.mobile?($("
").attr("id",e).appendTo("body"),{div_id:e,display_type:t,priority:p.MAXIMUM_PROMO_PRIORITY}):null}function b(e){this.id=e.id,this.div_id=e.div_id||"",this.html=e.html||"",this.display_type=e.display_type||"",this.click_handler=function(){"undefined"!=typeof ga?ga("rtfd.send","event","Promo","Click",e.id):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Promo","Click",e.id])}}b.prototype.display=function(){$("#"+this.div_id).html(this.html),$("#"+this.div_id).find('a[href*="/sustainability/click/"]').on("click",this.click_handler),this.post_promo_display()},b.prototype.disable=function(){$("#"+this.div_id).hide()},b.prototype.post_promo_display=function(){this.display_type===p.PROMO_TYPES.FOOTER&&($("
").insertAfter("#"+this.div_id),$("
").insertBefore("#"+this.div_id+".ethical-alabaster .ethical-footer"))},t.exports={Promo:b,init:function(){var e,t,i,r,n,o={format:"jsonp"},s=[],a=[],l=[],d=[g,m,v];if(u=h.get(),r="rtd-"+(Math.random()+1).toString(36).substring(4),n=p.PROMO_TYPES.LEFTNAV,i=u.is_rtd_like_theme()?"ethical-rtd ethical-dark-theme":"ethical-alabaster",t=0<$(f).length?($("
").attr("id",r).addClass(i).appendTo(f),{div_id:r,display_type:n}):null)s.push(t.div_id),a.push(t.display_type),l.push(t.priority||p.DEFAULT_PROMO_PRIORITY);else{if(!u.show_promo())return;for(var c=0;c").attr("id","rtd-detection").attr("class","ethical-rtd").html(" ").appendTo("body"),0===$("#rtd-detection").height()&&(i=!0),$("#rtd-detection").remove(),i)&&(console.log("---------------------------------------------------------------------------------------"),console.log("Read the Docs hosts documentation for tens of thousands of open source projects."),console.log("We fund our development (we are open source) and operations through advertising."),console.log("We promise to:"),console.log(" - never let advertisers run 3rd party JavaScript"),console.log(" - never sell user data to advertisers or other 3rd parties"),console.log(" - only show advertisements of interest to developers"),console.log("Read more about our approach to advertising here: https://docs.readthedocs.io/en/latest/ethical-advertising.html"),console.log("%cPlease allow our Ethical Ads or go ad-free:","font-size: 2em"),console.log("https://docs.readthedocs.io/en/latest/advertising/ad-blocking.html"),console.log("--------------------------------------------------------------------------------------"),e=m(),t=null,e&&e.div_id&&(t=$("#"+e.div_id).attr("class","keep-us-sustainable"),$("

").text("Support Read the Docs!").appendTo(t),$("

").html('Please help keep us sustainable by allowing our Ethical Ads in your ad blocker or go ad-free by subscribing.').appendTo(t),$("

").text("Thank you! ❤️").appendTo(t)))}})}}},{"./constants":13,"./rtd-data":15,bowser:7}],19:[function(e,t,i){var o=e("./rtd-data");t.exports={init:function(e){var t=o.get();if(!e.is_highest){var i=window.location.pathname.replace(t.version,e.slug),r=$('

Note

You are not reading the most recent version of this documentation. is the latest version available.

');r.find("a").attr("href",i).text(e.slug);var n=$("div.body");n.length||(n=$("div.document")),n.prepend(r)}}}},{"./rtd-data":15}],20:[function(e,t,i){var r=e("./doc-embed/sponsorship"),n=e("./doc-embed/footer.js"),o=(e("./doc-embed/rtd-data"),e("./doc-embed/sphinx")),s=e("./doc-embed/search");$(document).ready(function(){n.init(),o.init(),s.init(),r.init()})},{"./doc-embed/footer.js":14,"./doc-embed/rtd-data":15,"./doc-embed/search":16,"./doc-embed/sphinx":17,"./doc-embed/sponsorship":18}]},{},[20]); \ No newline at end of file +!function o(s,a,l){function d(t,e){if(!a[t]){if(!s[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(c)return c(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var n=a[t]={exports:{}};s[t][0].call(n.exports,function(e){return d(s[t][1][e]||e)},n,n.exports,o,s,a,l)}return a[t].exports}for(var c="function"==typeof require&&require,e=0;e
"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var t=i(this);expand=i(''),expand.on("click",function(e){return r.toggleCurrent(t),e.stopPropagation(),!1}),t.prepend(expand)})},reset:function(){var e=encodeURI(window.location.hash)||"#";try{var t=$(".wy-menu-vertical"),i=t.find('[href="'+e+'"]');if(0===i.length){var r=$('.document [id="'+e.substring(1)+'"]').closest("div.section");0===(i=t.find('[href="#'+r.attr("id")+'"]')).length&&(i=t.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=e)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(e){var t=e.closest("li");t.siblings("li.current").removeClass("current"),t.siblings().find("li.current").removeClass("current"),t.find("> ul li.current").removeClass("current"),t.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:t.exports.ThemeNav,StickyNav:t.exports.ThemeNav}),function(){for(var o=0,e=["ms","moz","webkit","o"],t=0;t/g,c=/"/g,p=/"/g,h=/&#([a-zA-Z0-9]*);?/gim,f=/:?/gim,m=/&newline;?/gim,g=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi,v=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi,b=/u\s*r\s*l\s*\(.*/gi;function w(e){return e.replace(c,""")}function y(e){return e.replace(p,'"')}function _(e){return e.replace(h,function(e,t){return"x"===t[0]||"X"===t[0]?String.fromCharCode(parseInt(t.substr(1),16)):String.fromCharCode(parseInt(t,10))})}function x(e){return e.replace(f,":").replace(m," ")}function k(e){for(var t="",i=0,r=e.length;i/g;i.whiteList={a:["target","href","title"],abbr:["title"],address:[],area:["shape","coords","href","alt"],article:[],aside:[],audio:["autoplay","controls","loop","preload","src"],b:[],bdi:["dir"],bdo:["dir"],big:[],blockquote:["cite"],br:[],caption:[],center:[],cite:[],code:[],col:["align","valign","span","width"],colgroup:["align","valign","span","width"],dd:[],del:["datetime"],details:["open"],div:[],dl:[],dt:[],em:[],font:["color","size","face"],footer:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],header:[],hr:[],i:[],img:["src","alt","title","width","height"],ins:["datetime"],li:[],mark:[],nav:[],ol:[],p:[],pre:[],s:[],section:[],small:[],span:[],sub:[],sup:[],strong:[],table:["width","border","align","valign"],tbody:["align","valign"],td:["width","rowspan","colspan","align","valign"],tfoot:["align","valign"],th:["width","rowspan","colspan","align","valign"],thead:["align","valign"],tr:["rowspan","align","valign"],tt:[],u:[],ul:[],video:["autoplay","controls","loop","preload","src","height","width"]},i.getDefaultWhiteList=o,i.onTag=function(e,t,i){},i.onIgnoreTag=function(e,t,i){},i.onTagAttr=function(e,t,i){},i.onIgnoreTagAttr=function(e,t,i){},i.safeAttrValue=function(e,t,i,r){if(i=T(i),"href"===t||"src"===t){if("#"===(i=u.trim(i)))return"#";if("http://"!==i.substr(0,7)&&"https://"!==i.substr(0,8)&&"mailto:"!==i.substr(0,7)&&"tel:"!==i.substr(0,4)&&"#"!==i[0]&&"/"!==i[0])return""}else if("background"===t){if(g.lastIndex=0,g.test(i))return""}else if("style"===t){if(v.lastIndex=0,v.test(i))return"";if(b.lastIndex=0,b.test(i)&&(g.lastIndex=0,g.test(i)))return"";!1!==r&&(i=(r=r||s).process(i))}return i=E(i)},i.escapeHtml=a,i.escapeQuote=w,i.unescapeQuote=y,i.escapeHtmlEntities=_,i.escapeDangerHtml5Entities=x,i.clearNonPrintableCharacter=k,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(s,a){"function"!=typeof a&&(a=function(){});var l=!Array.isArray(s),d=[],c=!1;return{onIgnoreTag:function(e,t,i){if(o=e,l||-1!==u.indexOf(s,o)){if(i.isClosing){var r="[/removed]",n=i.position+r.length;return d.push([!1!==c?c:i.position,n]),c=!1,r}return c||(c=i.position),"[removed]"}return a(e,t,i);var o},remove:function(t){var i="",r=0;return u.forEach(d,function(e){i+=t.slice(r,e[0]),r=e[1]}),i+=t.slice(r)}}},i.stripCommentTag=function(e){return e.replace(S,"")},i.stripBlankChar=function(e){var t=e.split("");return(t=t.filter(function(e){var t=e.charCodeAt(0);return!(127===t||t<=31&&10!==t&&13!==t)})).join("")},i.cssFilter=s,i.getDefaultCSSWhiteList=n},{"./util":5,cssfilter:10}],3:[function(e,t,i){var r=e("./default"),n=e("./parser"),o=e("./xss");for(var s in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,r)i[s]=r[s];for(var s in n)i[s]=n[s];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var c=e("./util");function p(e){var t=c.spaceIndex(e);if(-1===t)var i=e.slice(1,-1);else i=e.slice(1,t+1);return"/"===(i=c.trim(i).toLowerCase()).slice(0,1)&&(i=i.slice(1)),"/"===i.slice(-1)&&(i=i.slice(0,-1)),i}var u=/[^a-zA-Z0-9_:\.\-]/gim;function h(e,t){for(;t"===u){r+=i(e.slice(n,o)),c=p(d=e.slice(o,a+1)),r+=t(o,r.length,c,d,"";var a=function(e){var t=w.spaceIndex(e);if(-1===t)return{html:"",closing:"/"===e[e.length-2]};var i="/"===(e=w.trim(e.slice(t+1,-1)))[e.length-1];return i&&(e=w.trim(e.slice(0,-1))),{html:e,closing:i}}(i),l=c[n],d=b(a.html,function(e,t){var i,r=-1!==w.indexOf(l,e);return y(i=h(n,e,t,r))?r?(t=m(n,e,t,v))?e+'="'+t+'"':e:y(i=f(n,e,t,r))?void 0:i:i});i="<"+n;return d&&(i+=" "+d),a.closing&&(i+=" /"),i+=">"}return y(o=p(n,i,s))?g(i):o},g);return i&&(r=i.remove(r)),r},t.exports=a},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var r,n;r=this,n=function(){var T=!0;function s(i){function e(e){var t=i.match(e);return t&&1t[1][i])return 1;if(t[0][i]!==t[1][i])return-1;if(0===i)return 0}}function o(e,t,i){var r=a;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(r=s(i));var n=""+r.version;for(var o in e)if(e.hasOwnProperty(o)&&r[o]){if("string"!=typeof e[o])throw new Error("Browser version in the minVersion map should be a string: "+o+": "+String(e));return l([n,e[o]])<0}return t}return a.test=function(e){for(var t=0;t'),s=document.createElement("a"),a=r.highlight;if(s.href+=n.link+DOCUMENTATION_OPTIONS.FILE_SUFFIX,s.search="?highlight="+$.urlencode(d),o.append($("").attr("href",s).html(n.title)),-1===n.project.indexOf(c)&&o.append($("").text(" (from project "+n.project+")")),a.content.length){var l=$('
').html(u(a.content[0]));l.find("em").addClass("highlighted"),o.append(l)}Search.output.append(o),o.slideDown(5)}t.length?Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length)):Search.query_fallback(d)}).fail(function(e){Search.query_fallback(d)}).always(function(){$("#search-progress").empty(),Search.stopPulse(),Search.title.text(_("Search Results")),Search.status.fadeIn(500)}),$.ajax({url:e.href,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){return void 0===e.responseJSON||void 0===e.responseJSON.results?r.reject():r.resolve(e.responseJSON.results)}}).fail(function(e,t,i){return r.reject()})}}$(document).ready(function(){"undefined"!=typeof Search&&Search.init()})}(r.get())}}},{"./../../../../../../bower_components/xss/lib/index":3,"./rtd-data":15}],17:[function(n,e,t){var o=n("./rtd-data");e.exports={init:function(){var e=o.get();if($(document).on("click","[data-toggle='rst-current-version']",function(){var e=$("[data-toggle='rst-versions']").hasClass("shift-up")?"was_open":"was_closed";"undefined"!=typeof ga?ga("rtfd.send","event","Flyout","Click",e):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Flyout","Click",e])}),void 0===window.SphinxRtdTheme){var t=n("./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js").ThemeNav;if($(document).ready(function(){setTimeout(function(){t.navBar||t.enable()},1e3)}),e.is_rtd_like_theme()&&!$("div.wy-side-scroll:first").length){console.log("Applying theme sidebar fix...");var i=$("nav.wy-nav-side:first"),r=$("
").addClass("wy-side-scroll");i.children().detach().appendTo(r),r.prependTo(i),t.navBar=r}}}}},{"./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js":1,"./rtd-data":15}],18:[function(e,t,i){var u,p=e("./constants"),h=e("./rtd-data"),r=e("bowser"),f="#ethical-ad-placement";function m(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),r=p.PROMO_TYPES.LEFTNAV,n=p.DEFAULT_PROMO_PRIORITY,o=null;return u.is_mkdocs_builder()&&u.is_rtd_like_theme()?(o="nav.wy-nav-side",e="ethical-rtd ethical-dark-theme"):u.is_rtd_like_theme()?(o="nav.wy-nav-side > div.wy-side-scroll",e="ethical-rtd ethical-dark-theme"):u.is_alabaster_like_theme()&&(o="div.sphinxsidebar > div.sphinxsidebarwrapper",e="ethical-alabaster"),o?($("
").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top>$(window).height())&&(n=p.LOW_PROMO_PRIORITY),{div_id:i,display_type:r,priority:n}):null}function g(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),r=p.PROMO_TYPES.FOOTER,n=p.DEFAULT_PROMO_PRIORITY,o=null;return u.is_rtd_like_theme()?(o=$("
").insertAfter("footer hr"),e="ethical-rtd"):u.is_alabaster_like_theme()&&(o="div.bodywrapper .body",e="ethical-alabaster"),o?($("
").attr("id",i).addClass(e).appendTo(o),(!(t=$("#"+i).offset())||t.top<$(window).height())&&(n=p.LOW_PROMO_PRIORITY),{div_id:i,display_type:r,priority:n}):null}function v(){var e="rtd-"+(Math.random()+1).toString(36).substring(4),t=p.PROMO_TYPES.FIXED_FOOTER;return r&&r.mobile?($("
").attr("id",e).appendTo("body"),{div_id:e,display_type:t,priority:p.MAXIMUM_PROMO_PRIORITY}):null}function b(e){this.id=e.id,this.div_id=e.div_id||"",this.html=e.html||"",this.display_type=e.display_type||"",this.click_handler=function(){"undefined"!=typeof ga?ga("rtfd.send","event","Promo","Click",e.id):"undefined"!=typeof _gaq&&_gaq.push(["rtfd._setAccount","UA-17997319-1"],["rtfd._trackEvent","Promo","Click",e.id])}}b.prototype.display=function(){$("#"+this.div_id).html(this.html),$("#"+this.div_id).find('a[href*="/sustainability/click/"]').on("click",this.click_handler),this.post_promo_display()},b.prototype.disable=function(){$("#"+this.div_id).hide()},b.prototype.post_promo_display=function(){this.display_type===p.PROMO_TYPES.FOOTER&&($("
").insertAfter("#"+this.div_id),$("
").insertBefore("#"+this.div_id+".ethical-alabaster .ethical-footer"))},t.exports={Promo:b,init:function(){var e,t,i,r,n,o={format:"jsonp"},s=[],a=[],l=[],d=[g,m,v];if(u=h.get(),r="rtd-"+(Math.random()+1).toString(36).substring(4),n=p.PROMO_TYPES.LEFTNAV,i=u.is_rtd_like_theme()?"ethical-rtd ethical-dark-theme":"ethical-alabaster",t=0<$(f).length?($("
").attr("id",r).addClass(i).appendTo(f),{div_id:r,display_type:n}):null)s.push(t.div_id),a.push(t.display_type),l.push(t.priority||p.DEFAULT_PROMO_PRIORITY);else{if(!u.show_promo())return;for(var c=0;c").attr("id","rtd-detection").attr("class","ethical-rtd").html(" ").appendTo("body"),0===$("#rtd-detection").height()&&(i=!0),$("#rtd-detection").remove(),i)&&(console.log("---------------------------------------------------------------------------------------"),console.log("Read the Docs hosts documentation for tens of thousands of open source projects."),console.log("We fund our development (we are open source) and operations through advertising."),console.log("We promise to:"),console.log(" - never let advertisers run 3rd party JavaScript"),console.log(" - never sell user data to advertisers or other 3rd parties"),console.log(" - only show advertisements of interest to developers"),console.log("Read more about our approach to advertising here: https://docs.readthedocs.io/en/latest/ethical-advertising.html"),console.log("%cPlease allow our Ethical Ads or go ad-free:","font-size: 2em"),console.log("https://docs.readthedocs.io/en/latest/advertising/ad-blocking.html"),console.log("--------------------------------------------------------------------------------------"),e=m(),t=null,e&&e.div_id&&(t=$("#"+e.div_id).attr("class","keep-us-sustainable"),$("

").text("Support Read the Docs!").appendTo(t),$("

").html('Please help keep us sustainable by allowing our Ethical Ads in your ad blocker or go ad-free by subscribing.').appendTo(t),$("

").text("Thank you! ❤️").appendTo(t)))}})}}},{"./constants":13,"./rtd-data":15,bowser:7}],19:[function(e,t,i){var o=e("./rtd-data");t.exports={init:function(e){var t=o.get();if(!e.is_highest){var i=window.location.pathname.replace(t.version,e.slug),r=$('

Note

You are not reading the most recent version of this documentation. is the latest version available.

');r.find("a").attr("href",i).text(e.slug);var n=$("div.body");n.length||(n=$("div.document")),n.prepend(r)}}}},{"./rtd-data":15}],20:[function(e,t,i){var r=e("./doc-embed/sponsorship"),n=e("./doc-embed/footer.js"),o=(e("./doc-embed/rtd-data"),e("./doc-embed/sphinx")),s=e("./doc-embed/search");$(document).ready(function(){n.init(),o.init(),s.init(),r.init()})},{"./doc-embed/footer.js":14,"./doc-embed/rtd-data":15,"./doc-embed/search":16,"./doc-embed/sphinx":17,"./doc-embed/sponsorship":18}]},{},[20]); \ No newline at end of file diff --git a/readthedocs/projects/static/projects/js/tools.js b/readthedocs/projects/static/projects/js/tools.js index 47ef4576bfc..ec4ff8a1f40 100644 --- a/readthedocs/projects/static/projects/js/tools.js +++ b/readthedocs/projects/static/projects/js/tools.js @@ -1 +1 @@ -require=function o(i,a,l){function c(t,e){if(!a[t]){if(!i[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var s=a[t]={exports:{}};i[t][0].call(s.exports,function(e){return c(i[t][1][e]||e)},s,s.exports,o,i,a,l)}return a[t].exports}for(var u="function"==typeof require&&require,e=0;e'),i("body").append(t));var n=e.insertContent(t);i(n).show(),t.show(),i(document).click(function(e){i(e.target).closest("#embed-container").length||(i(n).remove(),t.remove())})}function s(e){var s=this;s.config=e||{},void 0===s.config.api_host&&(s.config.api_host="https://readthedocs.org"),s.help=o.observable(null),s.error=o.observable(null),s.project=o.observable(s.config.project),s.file=o.observable(null),s.sections=o.observableArray(),o.computed(function(){var e=s.file();(s.sections.removeAll(),e)&&(s.help("Loading..."),s.error(null),s.section(null),new r.Embed(s.config).page(s.project(),"latest",s.file(),function(e){s.sections.removeAll(),s.help(null),s.error(null);var t,n=[];for(t in e.sections){var r=e.sections[t];i.each(r,function(e,t){n.push({title:e,id:e})})}s.sections(n)},function(e){s.help(null),s.error("There was a problem retrieving data from the API")}))}),s.has_sections=o.computed(function(){return 0'),i("body").append(t));var n=e.insertContent(t);i(n).show(),t.show(),i(document).click(function(e){i(e.target).closest("#embed-container").length||(i(n).remove(),t.remove())})}function s(e){var s=this;s.config=e||{},void 0===s.config.api_host&&(s.config.api_host="https://readthedocs.org"),s.help=o.observable(null),s.error=o.observable(null),s.project=o.observable(s.config.project),s.file=o.observable(null),s.sections=o.observableArray(),o.computed(function(){var e=s.file();(s.sections.removeAll(),e)&&(s.help("Loading..."),s.error(null),s.section(null),new r.Embed(s.config).page(s.project(),"latest",s.file(),function(e){s.sections.removeAll(),s.help(null),s.error(null);var t,n=[];for(t in e.sections){var r=e.sections[t];i.each(r,function(e,t){n.push({title:e,id:e})})}s.sections(n)},function(e){s.help(null),s.error("There was a problem retrieving data from the API")}))}),s.has_sections=o.computed(function(){return 0