diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000000..0e55d253be9 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,6 @@ +version: 2 +formats: all +sphinx: + configuration: docs/conf.py +python: + requirements: requirements.txt 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/builds/models.py b/readthedocs/builds/models.py index ffd15ee6844..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( @@ -193,6 +212,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/builds/views.py b/readthedocs/builds/views.py index 473db6ac245..d8a9e7d45c5 100644 --- a/readthedocs/builds/views.py +++ b/readthedocs/builds/views.py @@ -1,26 +1,33 @@ +# -*- 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 +38,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 @@ -54,8 +63,10 @@ def post(self, request, project_slug): slug=version_slug, ) - trigger_build(project=project, version=version) - return HttpResponseRedirect(reverse('builds_project_list', args=[project.slug])) + _, build = trigger_build(project=project, version=version) + 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]) + ) 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) diff --git a/readthedocs/core/fixtures/eric.json b/readthedocs/core/fixtures/eric.json index c380e1cf5c5..8feffdce467 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": "pbkdf2_sha256$30000$Vs87OlKZEzCb$nUw1o5pGQw7ff/QhnleSpUOupBaT1DogZrVaoZyQRyc=", "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": "pbkdf2_sha256$30000$Vs87OlKZEzCb$nUw1o5pGQw7ff/QhnleSpUOupBaT1DogZrVaoZyQRyc=", "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": "pbkdf2_sha256$30000$Vs87OlKZEzCb$nUw1o5pGQw7ff/QhnleSpUOupBaT1DogZrVaoZyQRyc=", "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/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/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index 4c22e38dde0..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, ) @@ -151,9 +158,9 @@ 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( + 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() + 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/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/backends/sphinx.py b/readthedocs/doc_builder/backends/sphinx.py index 4dc29714218..3cfe5766dfe 100644 --- a/readthedocs/doc_builder/backends/sphinx.py +++ b/readthedocs/doc_builder/backends/sphinx.py @@ -108,6 +108,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/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 7e303e25f78..ce2ce3d844b 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 try again later. ' @@ -31,32 +29,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}', + 'Problem in your project\'s 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}', + ) 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): diff --git a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl index 4017453caff..a5f0d064c2b 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 @@ -39,31 +40,23 @@ 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: - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' + 'html_style' not in globals() + ) or 'html_theme' not in globals() +) +if using_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()] - using_rtd_theme = True + html_theme_path = [theme.get_html_theme_path()] if globals().get('websupport2_base_url', False): websupport2_base_url = '{{ api_host }}/websupport' 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 c2315af839f..3f7099febf2 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": "pbkdf2_sha256$30000$Vs87OlKZEzCb$nUw1o5pGQw7ff/QhnleSpUOupBaT1DogZrVaoZyQRyc=", "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/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..7e9b48da270 --- /dev/null +++ b/readthedocs/projects/migrations/0030_change-max-length-project-slug.py @@ -0,0 +1,51 @@ +# -*- 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() + + 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): + + 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'), + ), + migrations.AlterField( + model_name='project', + name='name', + field=models.CharField(max_length=63, verbose_name='Name'), + ), + ] diff --git a/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py b/readthedocs/projects/migrations/0031_add_modified_date_importedfile.py new file mode 100644 index 00000000000..255da1c003a --- /dev/null +++ b/readthedocs/projects/migrations/0031_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', '0030_change-max-length-project-slug'), + ] + + 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..0ff520dd565 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -80,8 +80,9 @@ 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) - slug = models.SlugField(_('Slug'), max_length=255, unique=True) + # 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 ' 'description of the project')) @@ -943,6 +944,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) 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 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/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): 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 %} diff --git a/readthedocs/rtd_tests/tests/test_footer.py b/readthedocs/rtd_tests/tests/test_footer.py index 82e1b780b19..2b63301753f 100644 --- a/readthedocs/rtd_tests/tests/test_footer.py +++ b/readthedocs/rtd_tests/tests/test_footer.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import ( - absolute_import, division, print_function, unicode_literals) + absolute_import, + division, + print_function, + unicode_literals, +) import mock from django.test import TestCase @@ -11,13 +15,15 @@ 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 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 +105,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'] diff --git a/readthedocs/rtd_tests/tests/test_project.py b/readthedocs/rtd_tests/tests/test_project.py index 5a723cccd23..5029eb58630 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 656dd103a86..6e9a6eaa8a8 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 @@ -8,6 +9,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, new import six @@ -205,7 +207,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 @@ -227,7 +229,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/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, + ) 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 4770c009337..96c84e3c2e0 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -209,7 +209,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')), diff --git a/setup.cfg b/setup.cfg index b323f46bd49..3f91509df4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = readthedocs -version = 2.8.0 +version = 2.8.1 license = MIT description = Read the Docs builds and hosts documentation author = Read the Docs, Inc