diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b8c6e18632d..9d6454f44e9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,21 @@ +Version 5.1.1 +------------- + +:Date: May 26, 2020 + +* `@stsewd `__: Search: show total_results from last query (`#7101 `__) +* `@humitos `__: Add a tip in EmbedAPI to use Sphinx reference in section (`#7099 `__) +* `@ericholscher `__: Release 5.1.0 (`#7098 `__) +* `@ericholscher `__: Add a setting for storing pageviews (`#7097 `__) +* `@humitos `__: Document Embed APIv2 endpoint (`#7095 `__) +* `@stsewd `__: Footer: Check for mkdocs doctype too (`#7094 `__) +* `@ericholscher `__: Fix the unresolver not working properly with root paths (`#7093 `__) +* `@ericholscher `__: Add a project-level configuration for PR builds (`#7090 `__) +* `@santos22 `__: Fix tests ahead of django-dynamic-fixture update (`#7073 `__) +* `@ericholscher `__: Add ability for users to set their own URLConf (`#6963 `__) +* `@dojutsu-user `__: Store Pageviews in DB (`#6121 `__) +* `@humitos `__: GitLab Integration (`#3327 `__) + Version 5.1.0 ------------- diff --git a/docs/api/v2.rst b/docs/api/v2.rst index 08f7d19290e..9acf1587a0c 100644 --- a/docs/api/v2.rst +++ b/docs/api/v2.rst @@ -317,6 +317,85 @@ Build detail Some fields primarily used for UI elements in Read the Docs are omitted. + +Embed +~~~~~ + +.. http:get:: /api/v2/embed/ + + Retrieve HTML-formatted content from documentation page or section. + + **Example request**: + + .. prompt:: bash $ + + curl https://readthedocs.org/api/v2/embed/?project=docs&version=latest&doc=features&path=features.html + + or + + .. prompt:: bash $ + + curl https://readthedocs.org/api/v2/embed/?url=https://docs.readthedocs.io/en/latest/features.html + + **Example response**: + + .. sourcecode:: js + + { + "content": [ + "
\n

Read the Docs..." + ], + "headers": [ + { + "Read the Docs features": "#" + }, + { + "Automatic Documentation Deployment": "#automatic-documentation-deployment" + }, + { + "Custom Domains & White Labeling": "#custom-domains-white-labeling" + }, + { + "Versioned Documentation": "#versioned-documentation" + }, + { + "Downloadable Documentation": "#downloadable-documentation" + }, + { + "Full-Text Search": "#full-text-search" + }, + { + "Open Source and Customer Focused": "#open-source-and-customer-focused" + } + ], + "url": "https://docs.readthedocs.io/en/latest/features", + "meta": { + "project": "docs", + "version": "latest", + "doc": "features", + "section": "read the docs features" + } + } + + :>json string content: HTML content of the section. + :>json object headers: section's headers in the document. + :>json string url: URL of the document. + :>json object meta: meta data of the requested section. + + :query string project: Read the Docs project's slug. + :query string doc: document to fetch content from. + + :query string version: *optional* Read the Docs version's slug (default: ``latest``). + :query string section: *optional* section within the document to fetch. + :query string path: *optional* full path to the document including extension. + + :query string url: full URL of the document (and section) to fetch content from. + + .. note:: + + You can call this endpoint by sending at least ``project`` and ``doc`` *or* ``url`` attribute. + + Undocumented resources and endpoints ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/guides/autobuild-docs-for-pull-requests.rst b/docs/guides/autobuild-docs-for-pull-requests.rst index 0fa7fd3c900..2d82200619c 100644 --- a/docs/guides/autobuild-docs-for-pull-requests.rst +++ b/docs/guides/autobuild-docs-for-pull-requests.rst @@ -2,8 +2,11 @@ Autobuild Documentation for Pull Requests ========================================= Read the Docs allows autobuilding documentation for pull/merge requests for GitHub or GitLab projects. -This feature is currently available under a :doc:`Feature Flag `. -So, you can enable this feature by sending us an `email `__ including your project URL. +This feature is currently enabled for a subset of our projects while being rolled out. +You can check to see if your project has it enabled by looking at the :guilabel:`Admin > Advanced settings` and look for :guilabel:`Build pull requests for this project`. +We are rolling this feature out based on the projects age on Read the Docs, +so older projects will get it first. +You can also ask for this feature by sending us an `email `__ including your project URL. Features -------- diff --git a/docs/guides/embedding-content.rst b/docs/guides/embedding-content.rst index 89768aaaf0f..3ce97910fb9 100644 --- a/docs/guides/embedding-content.rst +++ b/docs/guides/embedding-content.rst @@ -73,6 +73,32 @@ from our own docs and will populate the content of it into the ``#help-container You can modify this example to subscribe to ``.onclick`` Javascript event, and show a modal when the user clicks in a "Help" link. +.. tip:: + + Take into account that if the title changes, your ``section`` argument will break. + To avoid that, you can manually define Sphinx references above the sections you don't want to break. + For example, + + .. code-block:: rst + :emphasize-lines: 3 + + .. in your .rst document file + + .. _unbreakable-section-reference: + + Creating an automation rule + --------------------------- + + This is the text of the section. + + To link to the section "Creating an automation rule" you can send ``section=unbreakable-section-reference``. + If you change the title it won't break the embedded content because the label for that title will still be ``unbreakable-section-reference``. + + Please, take a look at the Sphinx `:ref:` `role documentation`_ for more information about how to create references. + + .. _role documentation: https://www.sphinx-doc.org/en/stable/usage/restructuredtext/roles.html#role-ref + + Calling the Embed API directly ------------------------------ diff --git a/readthedocs/api/v2/serializers.py b/readthedocs/api/v2/serializers.py index 5a26befac6e..da21980096f 100644 --- a/readthedocs/api/v2/serializers.py +++ b/readthedocs/api/v2/serializers.py @@ -27,6 +27,7 @@ class Meta: 'documentation_type', 'users', 'canonical_url', + 'urlconf', ) diff --git a/readthedocs/api/v2/views/integrations.py b/readthedocs/api/v2/views/integrations.py index d4438e990ce..e08a01c2125 100644 --- a/readthedocs/api/v2/views/integrations.py +++ b/readthedocs/api/v2/views/integrations.py @@ -412,6 +412,7 @@ def handle_webhook(self): # Handle pull request events if all([ self.project.has_feature(Feature.EXTERNAL_VERSION_BUILD), + self.project.external_builds_enabled, event == GITHUB_PULL_REQUEST, action, ]): @@ -568,6 +569,7 @@ def handle_webhook(self): if ( self.project.has_feature(Feature.EXTERNAL_VERSION_BUILD) and + self.project.external_builds_enabled and event == GITLAB_MERGE_REQUEST and action ): if ( diff --git a/readthedocs/core/resolver.py b/readthedocs/core/resolver.py index ce68f9de26a..7c62b2e6684 100644 --- a/readthedocs/core/resolver.py +++ b/readthedocs/core/resolver.py @@ -62,6 +62,7 @@ def base_resolve_path( subproject_slug=None, subdomain=None, cname=None, + urlconf=None, ): """Resolve a with nothing smart, just filling in the blanks.""" # Only support `/docs/project' URLs outside our normal environment. Normally @@ -79,6 +80,31 @@ def base_resolve_path( else: url += '{language}/{version_slug}/{filename}' + # Allow users to override their own URLConf + # This logic could be cleaned up with a standard set of variable replacements + if urlconf: + url = urlconf + url = url.replace( + '$version', + '{version_slug}', + ) + url = url.replace( + '$language', + '{language}', + ) + url = url.replace( + '$filename', + '{filename}', + ) + url = url.replace( + '$subproject', + '{subproject_slug}', + ) + if '$' in url: + log.warning( + 'Unconverted variable in a resolver URLConf: url=%s', url + ) + return url.format( project_slug=project_slug, filename=filename, @@ -97,6 +123,7 @@ def resolve_path( single_version=None, subdomain=None, cname=None, + urlconf=None, ): """Resolve a URL with a subset of fields defined.""" version_slug = version_slug or project.get_default_version() @@ -122,6 +149,7 @@ def resolve_path( subproject_slug=subproject_slug, cname=cname, subdomain=subdomain, + urlconf=urlconf or project.urlconf, ) def resolve_domain(self, project): diff --git a/readthedocs/core/static-src/core/js/doc-embed/rtd-data.js b/readthedocs/core/static-src/core/js/doc-embed/rtd-data.js index daa26465e69..90294e489ff 100644 --- a/readthedocs/core/static-src/core/js/doc-embed/rtd-data.js +++ b/readthedocs/core/static-src/core/js/doc-embed/rtd-data.js @@ -59,8 +59,10 @@ function get() { $.extend(config, defaults, window.READTHEDOCS_DATA); - // Force to use new settings - config.proxied_api_host = '/_'; + if (!("proxied_api_host" in config)) { + // Use direct proxied API host + config.proxied_api_host = '/_'; + } return config; } diff --git a/readthedocs/core/static/core/js/readthedocs-doc-embed.js b/readthedocs/core/static/core/js/readthedocs-doc-embed.js index 97675bf4529..4428633e78b 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 n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var r=a[t]={exports:{}};s[t][0].call(r.exports,function(e){return d(s[t][1][e]||e)},r,r.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 n.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 n=$('.document [id="'+e.substring(1)+'"]').closest("div.section");0===(i=t.find('[href="#'+n.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,u=/"/g,h=/"/g,p=/&#([a-zA-Z0-9]*);?/gim,f=/:?/gim,g=/&newline;?/gim,m=/((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,w=/u\s*r\s*l\s*\(.*/gi;function b(e){return e.replace(u,""")}function _(e){return e.replace(h,'"')}function y(e){return e.replace(p,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(g," ")}function k(e){for(var t="",i=0,n=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,n){if(i=T(i),"href"===t||"src"===t){if("#"===(i=c.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(m.lastIndex=0,m.test(i))return""}else if("style"===t){if(v.lastIndex=0,v.test(i))return"";if(w.lastIndex=0,w.test(i)&&(m.lastIndex=0,m.test(i)))return"";!1!==n&&(i=(n=n||s).process(i))}return i=E(i)},i.escapeHtml=a,i.escapeQuote=b,i.unescapeQuote=_,i.escapeHtmlEntities=y,i.escapeDangerHtml5Entities=x,i.clearNonPrintableCharacter=k,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(o,s){"function"!=typeof s&&(s=function(){});var a=!Array.isArray(o),l=[],d=!1;return{onIgnoreTag:function(e,t,i){if(function(e){return a||-1!==c.indexOf(o,e)}(e)){if(i.isClosing){var n="[/removed]",r=i.position+n.length;return l.push([!1!==d?d:i.position,r]),d=!1,n}return d=d||i.position,"[removed]"}return s(e,t,i)},remove:function(t){var i="",n=0;return c.forEach(l,function(e){i+=t.slice(n,e[0]),n=e[1]}),i+=t.slice(n)}}},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=r},{"./util":5,cssfilter:10}],3:[function(e,t,i){var n=e("./default"),r=e("./parser"),o=e("./xss");for(var s in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,n)i[s]=n[s];for(var s in r)i[s]=r[s];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var c=e("./util");function h(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 p(e,t){for(;t"===u){n+=i(e.slice(r,o)),c=h(d=e.slice(o,a+1)),n+=t(o,n.length,c,d,"";var a=function(e){var t=b.spaceIndex(e);if(-1===t)return{html:"",closing:"/"===e[e.length-2]};var i="/"===(e=b.trim(e.slice(t+1,-1)))[e.length-1];return i&&(e=b.trim(e.slice(0,-1))),{html:e,closing:i}}(i),l=c[r],d=w(a.html,function(e,t){var i,n=-1!==b.indexOf(l,e);return _(i=p(r,e,t,n))?n?(t=g(r,e,t,v))?e+'="'+t+'"':e:_(i=f(r,e,t,n))?void 0:i:i});i="<"+r;return d&&(i+=" "+d),a.closing&&(i+=" /"),i+=">"}return _(o=h(r,i,s))?m(i):o},m);return i&&(n=i.remove(n)),n},t.exports=a},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var n,r;n=this,r=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 n=a;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(n=s(i));var r=""+n.version;for(var o in e)if(e.hasOwnProperty(o)&&n[o]){if("string"!=typeof e[o])throw new Error("Browser version in the minVersion map should be a string: "+o+": "+String(e));return E([r,e[o]])<0}return t}return a.test=function(e){for(var t=0;t'),a=n.title;r&&r.title&&(a=O(r.title[0]));var l=n.link+"?highlight="+$.urlencode(A),d=$("",{href:l});if(d.html(a),d.find("span").addClass("highlighted"),s.append(d),n.project!==S){var c=" (from project "+n.project+")",u=$("",{text:c});s.append(u)}for(var h=0;h'),f="",g="",m="",v="",w="",b="",y="",x="",k="",T="";if("sections"===o[h].type){if(g=(f=o[h])._source.title,m=l+"#"+f._source.id,v=[f._source.content.substr(0,C)+" ..."],f.highlight&&(f.highlight["sections.title"]&&(g=O(f.highlight["sections.title"][0])),f.highlight["sections.content"])){w=f.highlight["sections.content"],v=[];for(var E=0;E<%= section_subtitle %><% for (var i = 0; i < section_content.length; ++i) { %>
<%= section_content[i] %>
<% } %>',{section_subtitle_link:m,section_subtitle:g,section_content:v})}"domains"===o[h].type&&(y=(b=o[h])._source.role_name,x=l+"#"+b._source.anchor,k=b._source.name,(T="")!==b._source.docstrings&&(T=b._source.docstrings.substr(0,C)+" ..."),b.highlight&&(b.highlight["domains.docstrings"]&&(T="... "+O(b.highlight["domains.docstrings"][0])+" ..."),b.highlight["domains.name"]&&(k=O(b.highlight["domains.name"][0]))),M(p,'
<%= domain_content %>
',{domain_subtitle_link:x,domain_subtitle:"["+y+"]: "+k,domain_content:T})),p.find("span").addClass("highlighted"),s.append(p),h!==o.length-1&&s.append($("
"))}Search.output.append(s),s.slideDown(5)}t.length?Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length)):(Search.query_fallback(A),console.log("Read the Docs search failed. Falling back to Sphinx search."))}).fail(function(e){Search.query_fallback(A)}).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"success"!==t||void 0===e.responseJSON||0===e.responseJSON.count?n.reject():n.resolve(e.responseJSON)}}).fail(function(e,t,i){return n.reject()})}}$(document).ready(function(){"undefined"!=typeof Search&&Search.init()})}(n.get())}}},{"./../../../../../../bower_components/xss/lib/index":3,"./rtd-data":16}],18:[function(r,e,t){var o=r("./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=r("./../../../../../../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())if(!$("div.wy-side-scroll:first").length){console.log("Applying theme sidebar fix...");var i=$("nav.wy-nav-side:first"),n=$("
").addClass("wy-side-scroll");i.children().detach().appendTo(n),n.prependTo(i),t.navBar=n}}}}},{"./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js":1,"./rtd-data":16}],19:[function(e,t,i){var l,d=e("./constants"),c=e("./rtd-data"),n=e("bowser"),u="#ethical-ad-placement";function h(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=d.PROMO_TYPES.LEFTNAV,r=d.DEFAULT_PROMO_PRIORITY,o=null;return l.is_mkdocs_builder()&&l.is_rtd_like_theme()?(o="nav.wy-nav-side",e="ethical-rtd ethical-dark-theme"):l.is_rtd_like_theme()?(o="nav.wy-nav-side > div.wy-side-scroll",e="ethical-rtd ethical-dark-theme"):l.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())&&(r=d.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function p(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=d.PROMO_TYPES.FOOTER,r=d.DEFAULT_PROMO_PRIORITY,o=null;return l.is_rtd_like_theme()?(o=$("
").insertAfter("footer hr"),e="ethical-rtd"):l.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())&&(r=d.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function f(){var e="rtd-"+(Math.random()+1).toString(36).substring(4),t=d.PROMO_TYPES.FIXED_FOOTER,i=d.DEFAULT_PROMO_PRIORITY;return n&&n.mobile&&(i=d.MAXIMUM_PROMO_PRIORITY),$("
").attr("id",e).appendTo("body"),{div_id:e,display_type:t,priority:i}}function g(e){this.id=e.id,this.div_id=e.div_id||"",this.html=e.html||"",this.display_type=e.display_type||"",this.view_tracking_url=e.view_url,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])}}g.prototype.display=function(){var e="#"+this.div_id,t=this.view_tracking_url;$(e).html(this.html),$(e).find('a[href*="/sustainability/click/"]').on("click",this.click_handler);function i(){$.inViewport($(e),-3)&&($("").attr("src",t).css("display","none").appendTo(e),$(window).off(".rtdinview"),$(".wy-side-scroll").off(".rtdinview"))}$(window).on("DOMContentLoaded.rtdinview load.rtdinview scroll.rtdinview resize.rtdinview",i),$(".wy-side-scroll").on("scroll.rtdinview",i),$(".ethical-close").on("click",function(){return $(e).hide(),!1}),this.post_promo_display()},g.prototype.disable=function(){$("#"+this.div_id).hide()},g.prototype.post_promo_display=function(){this.display_type===d.PROMO_TYPES.FOOTER&&($("
").insertAfter("#"+this.div_id),$("
").insertBefore("#"+this.div_id+".ethical-alabaster .ethical-footer"))},t.exports={Promo:g,init:function(){var e,t,i={format:"jsonp"},n=[],r=[],o=[],s=[p,h,f];if(l=c.get(),t=function(){var e,t="rtd-"+(Math.random()+1).toString(36).substring(4),i=d.PROMO_TYPES.LEFTNAV;return e=l.is_rtd_like_theme()?"ethical-rtd ethical-dark-theme":"ethical-alabaster",0<$(u).length?($("
").attr("id",t).addClass(e).appendTo(u),{div_id:t,display_type:i}):null}())n.push(t.div_id),r.push(t.display_type),o.push(t.priority||d.DEFAULT_PROMO_PRIORITY),!0;else{if(!l.show_promo())return;for(var a=0;a").attr("id","rtd-detection").attr("class","ethical-rtd").html(" ").appendTo("body"),0===$("#rtd-detection").height()&&(e=!0),$("#rtd-detection").remove(),e}()&&(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/advertising/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("--------------------------------------------------------------------------------------"),function(){var e=h(),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":14,"./rtd-data":16,bowser:7}],20:[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),n=$('

Note

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

');n.find("a").attr("href",i).text(e.slug);var r=$("div.body");r.length||(r=$("div.document")),r.prepend(n)}}}},{"./rtd-data":16}],21:[function(e,t,i){var n=e("./doc-embed/sponsorship"),r=e("./doc-embed/footer.js"),o=(e("./doc-embed/rtd-data"),e("./doc-embed/sphinx")),s=e("./doc-embed/search");$.extend(e("verge")),$(document).ready(function(){r.init(),o.init(),s.init(),n.init()})},{"./doc-embed/footer.js":15,"./doc-embed/rtd-data":16,"./doc-embed/search":17,"./doc-embed/sphinx":18,"./doc-embed/sponsorship":19,verge:13}]},{},[21]); \ 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 n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var r=a[t]={exports:{}};s[t][0].call(r.exports,function(e){return d(s[t][1][e]||e)},r,r.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 n.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 n=$('.document [id="'+e.substring(1)+'"]').closest("div.section");0===(i=t.find('[href="#'+n.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,u=/"/g,h=/"/g,p=/&#([a-zA-Z0-9]*);?/gim,f=/:?/gim,g=/&newline;?/gim,m=/((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,w=/u\s*r\s*l\s*\(.*/gi;function b(e){return e.replace(u,""")}function _(e){return e.replace(h,'"')}function y(e){return e.replace(p,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(g," ")}function k(e){for(var t="",i=0,n=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,n){if(i=T(i),"href"===t||"src"===t){if("#"===(i=c.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(m.lastIndex=0,m.test(i))return""}else if("style"===t){if(v.lastIndex=0,v.test(i))return"";if(w.lastIndex=0,w.test(i)&&(m.lastIndex=0,m.test(i)))return"";!1!==n&&(i=(n=n||s).process(i))}return i=E(i)},i.escapeHtml=a,i.escapeQuote=b,i.unescapeQuote=_,i.escapeHtmlEntities=y,i.escapeDangerHtml5Entities=x,i.clearNonPrintableCharacter=k,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(o,s){"function"!=typeof s&&(s=function(){});var a=!Array.isArray(o),l=[],d=!1;return{onIgnoreTag:function(e,t,i){if(function(e){return a||-1!==c.indexOf(o,e)}(e)){if(i.isClosing){var n="[/removed]",r=i.position+n.length;return l.push([!1!==d?d:i.position,r]),d=!1,n}return d=d||i.position,"[removed]"}return s(e,t,i)},remove:function(t){var i="",n=0;return c.forEach(l,function(e){i+=t.slice(n,e[0]),n=e[1]}),i+=t.slice(n)}}},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=r},{"./util":5,cssfilter:10}],3:[function(e,t,i){var n=e("./default"),r=e("./parser"),o=e("./xss");for(var s in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,n)i[s]=n[s];for(var s in r)i[s]=r[s];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var c=e("./util");function h(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 p(e,t){for(;t"===u){n+=i(e.slice(r,o)),c=h(d=e.slice(o,a+1)),n+=t(o,n.length,c,d,"";var a=function(e){var t=b.spaceIndex(e);if(-1===t)return{html:"",closing:"/"===e[e.length-2]};var i="/"===(e=b.trim(e.slice(t+1,-1)))[e.length-1];return i&&(e=b.trim(e.slice(0,-1))),{html:e,closing:i}}(i),l=c[r],d=w(a.html,function(e,t){var i,n=-1!==b.indexOf(l,e);return _(i=p(r,e,t,n))?n?(t=g(r,e,t,v))?e+'="'+t+'"':e:_(i=f(r,e,t,n))?void 0:i:i});i="<"+r;return d&&(i+=" "+d),a.closing&&(i+=" /"),i+=">"}return _(o=h(r,i,s))?m(i):o},m);return i&&(n=i.remove(n)),n},t.exports=a},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var n,r;n=this,r=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 n=a;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(n=s(i));var r=""+n.version;for(var o in e)if(e.hasOwnProperty(o)&&n[o]){if("string"!=typeof e[o])throw new Error("Browser version in the minVersion map should be a string: "+o+": "+String(e));return E([r,e[o]])<0}return t}return a.test=function(e){for(var t=0;t'),a=n.title;r&&r.title&&(a=O(r.title[0]));var l=n.link+"?highlight="+$.urlencode(A),d=$("",{href:l});if(d.html(a),d.find("span").addClass("highlighted"),s.append(d),n.project!==S){var c=" (from project "+n.project+")",u=$("",{text:c});s.append(u)}for(var h=0;h'),f="",g="",m="",v="",w="",b="",y="",x="",k="",T="";if("sections"===o[h].type){if(g=(f=o[h])._source.title,m=l+"#"+f._source.id,v=[f._source.content.substr(0,C)+" ..."],f.highlight&&(f.highlight["sections.title"]&&(g=O(f.highlight["sections.title"][0])),f.highlight["sections.content"])){w=f.highlight["sections.content"],v=[];for(var E=0;E<%= section_subtitle %>
<% for (var i = 0; i < section_content.length; ++i) { %>
<%= section_content[i] %>
<% } %>',{section_subtitle_link:m,section_subtitle:g,section_content:v})}"domains"===o[h].type&&(y=(b=o[h])._source.role_name,x=l+"#"+b._source.anchor,k=b._source.name,(T="")!==b._source.docstrings&&(T=b._source.docstrings.substr(0,C)+" ..."),b.highlight&&(b.highlight["domains.docstrings"]&&(T="... "+O(b.highlight["domains.docstrings"][0])+" ..."),b.highlight["domains.name"]&&(k=O(b.highlight["domains.name"][0]))),M(p,'
<%= domain_content %>
',{domain_subtitle_link:x,domain_subtitle:"["+y+"]: "+k,domain_content:T})),p.find("span").addClass("highlighted"),s.append(p),h!==o.length-1&&s.append($("
"))}Search.output.append(s),s.slideDown(5)}t.length?Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length)):(Search.query_fallback(A),console.log("Read the Docs search failed. Falling back to Sphinx search."))}).fail(function(e){Search.query_fallback(A)}).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"success"!==t||void 0===e.responseJSON||0===e.responseJSON.count?n.reject():n.resolve(e.responseJSON)}}).fail(function(e,t,i){return n.reject()})}}$(document).ready(function(){"undefined"!=typeof Search&&Search.init()})}(n.get())}}},{"./../../../../../../bower_components/xss/lib/index":3,"./rtd-data":16}],18:[function(r,e,t){var o=r("./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=r("./../../../../../../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())if(!$("div.wy-side-scroll:first").length){console.log("Applying theme sidebar fix...");var i=$("nav.wy-nav-side:first"),n=$("
").addClass("wy-side-scroll");i.children().detach().appendTo(n),n.prependTo(i),t.navBar=n}}}}},{"./../../../../../../bower_components/sphinx-rtd-theme/js/theme.js":1,"./rtd-data":16}],19:[function(e,t,i){var l,d=e("./constants"),c=e("./rtd-data"),n=e("bowser"),u="#ethical-ad-placement";function h(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=d.PROMO_TYPES.LEFTNAV,r=d.DEFAULT_PROMO_PRIORITY,o=null;return l.is_mkdocs_builder()&&l.is_rtd_like_theme()?(o="nav.wy-nav-side",e="ethical-rtd ethical-dark-theme"):l.is_rtd_like_theme()?(o="nav.wy-nav-side > div.wy-side-scroll",e="ethical-rtd ethical-dark-theme"):l.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())&&(r=d.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function p(){var e,t,i="rtd-"+(Math.random()+1).toString(36).substring(4),n=d.PROMO_TYPES.FOOTER,r=d.DEFAULT_PROMO_PRIORITY,o=null;return l.is_rtd_like_theme()?(o=$("
").insertAfter("footer hr"),e="ethical-rtd"):l.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())&&(r=d.LOW_PROMO_PRIORITY),{div_id:i,display_type:n,priority:r}):null}function f(){var e="rtd-"+(Math.random()+1).toString(36).substring(4),t=d.PROMO_TYPES.FIXED_FOOTER,i=d.DEFAULT_PROMO_PRIORITY;return n&&n.mobile&&(i=d.MAXIMUM_PROMO_PRIORITY),$("
").attr("id",e).appendTo("body"),{div_id:e,display_type:t,priority:i}}function g(e){this.id=e.id,this.div_id=e.div_id||"",this.html=e.html||"",this.display_type=e.display_type||"",this.view_tracking_url=e.view_url,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])}}g.prototype.display=function(){var e="#"+this.div_id,t=this.view_tracking_url;$(e).html(this.html),$(e).find('a[href*="/sustainability/click/"]').on("click",this.click_handler);function i(){$.inViewport($(e),-3)&&($("").attr("src",t).css("display","none").appendTo(e),$(window).off(".rtdinview"),$(".wy-side-scroll").off(".rtdinview"))}$(window).on("DOMContentLoaded.rtdinview load.rtdinview scroll.rtdinview resize.rtdinview",i),$(".wy-side-scroll").on("scroll.rtdinview",i),$(".ethical-close").on("click",function(){return $(e).hide(),!1}),this.post_promo_display()},g.prototype.disable=function(){$("#"+this.div_id).hide()},g.prototype.post_promo_display=function(){this.display_type===d.PROMO_TYPES.FOOTER&&($("
").insertAfter("#"+this.div_id),$("
").insertBefore("#"+this.div_id+".ethical-alabaster .ethical-footer"))},t.exports={Promo:g,init:function(){var e,t,i={format:"jsonp"},n=[],r=[],o=[],s=[p,h,f];if(l=c.get(),t=function(){var e,t="rtd-"+(Math.random()+1).toString(36).substring(4),i=d.PROMO_TYPES.LEFTNAV;return e=l.is_rtd_like_theme()?"ethical-rtd ethical-dark-theme":"ethical-alabaster",0<$(u).length?($("
").attr("id",t).addClass(e).appendTo(u),{div_id:t,display_type:i}):null}())n.push(t.div_id),r.push(t.display_type),o.push(t.priority||d.DEFAULT_PROMO_PRIORITY),!0;else{if(!l.show_promo())return;for(var a=0;a").attr("id","rtd-detection").attr("class","ethical-rtd").html(" ").appendTo("body"),0===$("#rtd-detection").height()&&(e=!0),$("#rtd-detection").remove(),e}()&&(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/advertising/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("--------------------------------------------------------------------------------------"),function(){var e=h(),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":14,"./rtd-data":16,bowser:7}],20:[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),n=$('

Note

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

');n.find("a").attr("href",i).text(e.slug);var r=$("div.body");r.length||(r=$("div.document")),r.prepend(n)}}}},{"./rtd-data":16}],21:[function(e,t,i){var n=e("./doc-embed/sponsorship"),r=e("./doc-embed/footer.js"),o=(e("./doc-embed/rtd-data"),e("./doc-embed/sphinx")),s=e("./doc-embed/search");$.extend(e("verge")),$(document).ready(function(){r.init(),o.init(),s.init(),n.init()})},{"./doc-embed/footer.js":15,"./doc-embed/rtd-data":16,"./doc-embed/search":17,"./doc-embed/sphinx":18,"./doc-embed/sponsorship":19,verge:13}]},{},[21]); \ No newline at end of file diff --git a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl index f8acd7242ec..5519dec08aa 100644 --- a/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl +++ b/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl @@ -97,6 +97,7 @@ context = { 'conf_py_path': '{{ conf_py_path }}', 'api_host': '{{ api_host }}', 'github_user': '{{ github_user }}', + 'proxied_api_host': '{{ project.proxied_api_host }}', 'github_repo': '{{ github_repo }}', 'github_version': '{{ github_version }}', 'display_github': {{ display_github }}, diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 138a40c2450..fcc271ce8f4 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -203,6 +203,7 @@ class Meta: 'analytics_code', 'show_version_warning', 'single_version', + 'external_builds_enabled' ) # These that can be set per-version using a config file. per_version_settings = ( @@ -259,6 +260,10 @@ def __init__(self, *args, **kwargs): else: self.fields['default_version'].widget.attrs['readonly'] = True + # Enable PR builder option on projects w/ feature flag + if not self.instance.has_feature(Feature.EXTERNAL_VERSION_BUILD): + self.fields.pop('external_builds_enabled') + def clean_conf_py_file(self): filename = self.cleaned_data.get('conf_py_file', '').strip() if filename and 'conf.py' not in filename: diff --git a/readthedocs/projects/migrations/0049_add_external_build_enabled.py b/readthedocs/projects/migrations/0049_add_external_build_enabled.py new file mode 100644 index 00000000000..04c7f6dcb28 --- /dev/null +++ b/readthedocs/projects/migrations/0049_add_external_build_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-05-21 22:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0048_remove_version_privacy_field'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='external_builds_enabled', + field=models.BooleanField(default=False, help_text='More information in our docs', null=True, verbose_name='Build pull requests for this project'), + ), + ] diff --git a/readthedocs/projects/migrations/0050_migrate_external_builds.py b/readthedocs/projects/migrations/0050_migrate_external_builds.py new file mode 100644 index 00000000000..21a45cd7400 --- /dev/null +++ b/readthedocs/projects/migrations/0050_migrate_external_builds.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.10 on 2020-05-19 13:24 + +from django.db import migrations + + +def migrate_features(apps, schema_editor): + # Enable the PR builder for projects with the feature flag + Feature = apps.get_model('projects', 'Feature') + if Feature.objects.filter(feature_id='external_version_build').exists(): + for project in Feature.objects.get(feature_id='external_version_build').projects.all(): + project.external_builds_enabled = True + project.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0049_add_external_build_enabled'), + ] + + operations = [ + migrations.RunPython(migrate_features), + ] diff --git a/readthedocs/projects/migrations/0051_project_urlconf_feature.py b/readthedocs/projects/migrations/0051_project_urlconf_feature.py new file mode 100644 index 00000000000..b32f9f1958b --- /dev/null +++ b/readthedocs/projects/migrations/0051_project_urlconf_feature.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-05-26 14:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0050_migrate_external_builds'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='urlconf', + field=models.CharField(default=None, help_text='Supports the following keys: $language, $version, $subproject, $filename. An example: `$language/$version/$filename`.', max_length=255, null=True, verbose_name='Documentation URL Configuration'), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 40af8e6fc44..db820482828 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -12,10 +12,12 @@ from django.core.files.storage import get_storage_class from django.db import models from django.db.models import Prefetch -from django.urls import NoReverseMatch, reverse +from django.urls import reverse, re_path +from django.conf.urls import include from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from django_extensions.db.models import TimeStampedModel +from django.views import defaults from shlex import quote from taggit.managers import TaggableManager @@ -23,6 +25,7 @@ from readthedocs.builds.constants import LATEST, STABLE, INTERNAL, EXTERNAL from readthedocs.core.resolver import resolve, resolve_domain from readthedocs.core.utils import broadcast, slugify +from readthedocs.constants import pattern_opts from readthedocs.doc_builder.constants import DOCKER_LIMITS from readthedocs.projects import constants from readthedocs.projects.exceptions import ProjectConfigurationError @@ -44,6 +47,7 @@ from readthedocs.vcs_support.backends import backend_cls from readthedocs.vcs_support.utils import Lock, NonBlockingLock + from .constants import ( MEDIA_TYPES, MEDIA_TYPE_PDF, @@ -53,7 +57,6 @@ log = logging.getLogger(__name__) -DOC_PATH_PREFIX = getattr(settings, 'DOC_PATH_PREFIX', '') class ProjectRelationship(models.Model): @@ -201,6 +204,24 @@ class Project(models.Model): 'DirectoryHTMLBuilder">More info on sphinx builders.', ), ) + urlconf = models.CharField( + _('Documentation URL Configuration'), + max_length=255, + default=None, + null=True, + help_text=_( + 'Supports the following keys: $language, $version, $subproject, $filename. ' + 'An example: `$language/$version/$filename`.' + ), + ) + + external_builds_enabled = models.BooleanField( + _('Build pull requests for this project'), + default=False, + # TODO: Remove this after migrations + null=True, + help_text=_('More information in our docs') # noqa + ) # Project features cdn_enabled = models.BooleanField(_('CDN Enabled'), default=False) @@ -541,13 +562,117 @@ def get_production_media_url(self, type_, version_slug): if self.is_subproject: # docs.example.com/_/downloads////pdf/ - path = f'//{domain}/{DOC_PATH_PREFIX}downloads/{self.alias}/{self.language}/{version_slug}/{type_}/' # noqa + path = f'//{domain}/{self.proxied_api_url}downloads/{self.alias}/{self.language}/{version_slug}/{type_}/' # noqa else: # docs.example.com/_/downloads///pdf/ - path = f'//{domain}/{DOC_PATH_PREFIX}downloads/{self.language}/{version_slug}/{type_}/' + path = f'//{domain}/{self.proxied_api_url}downloads/{self.language}/{version_slug}/{type_}/' # noqa return path + @property + def proxied_api_host(self): + """ + Used for the proxied_api_host in javascript. + + This needs to start with a slash at the root of the domain, + and end without a slash + """ + if self.urlconf: + # Add our proxied api host at the first place we have a $variable + # This supports both subpaths & normal root hosting + url_prefix = self.urlconf.split('$', 1)[0] + return '/' + url_prefix.strip('/') + '/_' + return '/_' + + @property + def proxied_api_url(self): + """ + Like the api_host but for use as a URL prefix. + + It can't start with a /, but has to end with one. + """ + return self.proxied_api_host.strip('/') + '/' + + @property + def regex_urlconf(self): + """ + Convert User's URLConf into a proper django URLConf. + + This replaces the user-facing syntax with the regex syntax. + """ + to_convert = self.urlconf + + # We should standardize these names so we can loop over them easier + to_convert = to_convert.replace( + '$version', + '(?P{regex})'.format(regex=pattern_opts['version_slug']) + ) + to_convert = to_convert.replace( + '$language', + '(?P{regex})'.format(regex=pattern_opts['lang_slug']) + ) + to_convert = to_convert.replace( + '$filename', + '(?P{regex})'.format(regex=pattern_opts['filename_slug']) + ) + to_convert = to_convert.replace( + '$subproject', + '(?P{regex})'.format(regex=pattern_opts['project_slug']) + ) + + if '$' in to_convert: + log.warning( + 'Unconverted variable in a project URLConf: project=%s to_convert=%s', + self, to_convert + ) + return to_convert + + @property + def proxito_urlconf(self): + """ + Returns a URLConf class that is dynamically inserted via proxito. + + It is used for doc serving on projects that have their own ``urlconf``. + """ + from readthedocs.projects.views.public import ProjectDownloadMedia + from readthedocs.proxito.views.serve import ServeDocs + from readthedocs.proxito.views.utils import proxito_404_page_handler + from readthedocs.proxito.urls import core_urls + + class ProxitoURLConf: + + """A URLConf dynamically inserted by Proxito.""" + + proxied_urls = [ + re_path( + r'{proxied_api_url}api/v2/'.format(proxied_api_url=self.proxied_api_url), + include('readthedocs.api.v2.proxied_urls'), + name='user_proxied_api' + ), + re_path( + r'{proxied_api_url}downloads/' + r'(?P{lang_slug})/' + r'(?P{version_slug})/' + r'(?P[-\w]+)/$'.format( + proxied_api_url=self.proxied_api_url, + **pattern_opts), + ProjectDownloadMedia.as_view(same_domain_url=True), + name='user_proxied_downloads' + ), + ] + docs_urls = [ + re_path( + '^{regex_urlconf}'.format(regex_urlconf=self.regex_urlconf), + ServeDocs.as_view(), + name='user_proxied_serve_docs' + ), + ] + urlpatterns = proxied_urls + core_urls + docs_urls + handler404 = proxito_404_page_handler + handler500 = defaults.server_error + + return ProxitoURLConf + @property def is_subproject(self): """Return whether or not this project is a subproject.""" diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index b23a4ef3654..8ef7570ebfe 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -6,7 +6,7 @@ from allauth.socialaccount.models import SocialAccount from django.conf import settings from django.contrib import messages -from django.db.models import Count +from django.db.models import Count, OuterRef, Subquery from django.http import ( Http404, HttpResponseBadRequest, @@ -39,10 +39,7 @@ Version, VersionAutomationRule, ) -from readthedocs.core.mixins import ( - ListViewWithForm, - PrivateViewMixin, -) +from readthedocs.core.mixins import ListViewWithForm, PrivateViewMixin from readthedocs.core.utils import broadcast, trigger_build from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.integrations.models import HttpExchange, Integration @@ -996,18 +993,21 @@ def get_context_data(self, **kwargs): project.slug, ) - queries = [] - qs = SearchQuery.objects.filter(project=project) - if qs.exists(): - qs = ( - qs.values('query') - .annotate(count=Count('id')) - .order_by('-count', 'query') - .values_list('query', 'count', 'total_results') + project_queries = SearchQuery.objects.filter(project=project) + last_total_results = ( + project_queries.filter(query=OuterRef('query')) + .order_by('-modified') + .values('total_results') + ) + queries = ( + project_queries.values('query') + .annotate( + count=Count('id'), + total_results=Subquery(last_total_results[:1]) ) - - # only show top 100 queries - queries = qs[:100] + .order_by('-count', 'query') + .values_list('query', 'count', 'total_results') + )[:100] context.update( { diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index d95eba84df5..50cf96e4ac2 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -6,6 +6,7 @@ Additional processing is done to get the project from the URL in the ``views.py`` as well. """ import logging +import sys from django.conf import settings from django.shortcuts import render @@ -166,6 +167,28 @@ def process_request(self, request): # noqa # Otherwise set the slug on the request request.host_project_slug = request.slug = ret + try: + project = Project.objects.get(slug=request.host_project_slug) + except Project.DoesNotExist: + log.exception('No host_project_slug set on project') + return None + + # This is hacky because Django wants a module for the URLConf, + # instead of also accepting string + if project.urlconf: + + # Stop Django from caching URLs + project_timestamp = project.modified_date.strftime("%Y%m%d.%H%M%S") + url_key = f'readthedocs.urls.fake.{project.slug}.{project_timestamp}' + + log.info( + 'Setting URLConf: project=%s url_key=%s urlconf=%s', + project, url_key, project.urlconf, + ) + if url_key not in sys.modules: + sys.modules[url_key] = project.proxito_urlconf + request.urlconf = url_key + return None def process_response(self, request, response): # noqa diff --git a/readthedocs/proxito/tests/test_middleware.py b/readthedocs/proxito/tests/test_middleware.py index a71d404cce0..2294563c6e1 100644 --- a/readthedocs/proxito/tests/test_middleware.py +++ b/readthedocs/proxito/tests/test_middleware.py @@ -1,18 +1,21 @@ # Copied from test_middleware.py +import sys + import pytest +from django.urls.base import set_urlconf, get_urlconf from django.test import TestCase from django.test.utils import override_settings from django_dynamic_fixture import get -from readthedocs.projects.models import Domain, Project +from readthedocs.projects.models import Domain, Project, ProjectRelationship from readthedocs.proxito.middleware import ProxitoMiddleware from readthedocs.rtd_tests.base import RequestFactoryTestMixin from readthedocs.rtd_tests.utils import create_user -@override_settings(PUBLIC_DOMAIN='dev.readthedocs.io') @pytest.mark.proxito +@override_settings(PUBLIC_DOMAIN='dev.readthedocs.io') class MiddlewareTests(RequestFactoryTestMixin, TestCase): def setUp(self): @@ -143,3 +146,108 @@ def test_long_bad_subdomain(self): request = self.request(self.url, HTTP_HOST=domain) res = self.run_middleware(request) self.assertEqual(res.status_code, 400) + + +@pytest.mark.proxito +@override_settings(PUBLIC_DOMAIN='dev.readthedocs.io') +class MiddlewareURLConfTests(RequestFactoryTestMixin, TestCase): + + def setUp(self): + self.owner = create_user(username='owner', password='test') + self.domain = 'pip.dev.readthedocs.io' + self.pip = get( + Project, + slug='pip', + users=[self.owner], + privacy_level='public', + urlconf='subpath/to/$version/$language/$filename' # Flipped + ) + + self.old_urlconf = get_urlconf() + sys.modules['fake_urlconf'] = self.pip.proxito_urlconf + set_urlconf('fake_urlconf') + + def tearDown(self): + set_urlconf(self.old_urlconf) + + def test_proxied_api_methods(self): + # This is mostly a unit test, but useful to make sure the below tests work + self.assertEqual(self.pip.proxied_api_url, 'subpath/to/_/') + self.assertEqual(self.pip.proxied_api_host, '/subpath/to/_') + + def test_middleware_urlconf(self): + resp = self.client.get('/subpath/to/testing/en/foodex.html', HTTP_HOST=self.domain) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp['X-Accel-Redirect'], + '/proxito/media/html/pip/testing/foodex.html', + ) + + def test_middleware_urlconf_invalid(self): + resp = self.client.get('/subpath/to/latest/index.html', HTTP_HOST=self.domain) + self.assertEqual(resp.status_code, 404) + + def test_middleware_urlconf_subpath_downloads(self): + # These aren't configurable yet + resp = self.client.get('/subpath/to/_/downloads/en/latest/pdf/', HTTP_HOST=self.domain) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp['X-Accel-Redirect'], + '/proxito/media/pdf/pip/latest/pip.pdf', + ) + + def test_middleware_urlconf_subpath_api(self): + # These aren't configurable yet + resp = self.client.get( + '/subpath/to/_/api/v2/footer_html/?project=pip&version=latest&language=en&page=index', + HTTP_HOST=self.domain + ) + self.assertEqual(resp.status_code, 200) + self.assertContains( + resp, + 'Inserted RTD Footer', + ) + + +@pytest.mark.proxito +@override_settings(PUBLIC_DOMAIN='dev.readthedocs.io') +class MiddlewareURLConfSubprojectTests(RequestFactoryTestMixin, TestCase): + + def setUp(self): + self.owner = create_user(username='owner', password='test') + self.domain = 'pip.dev.readthedocs.io' + self.pip = get( + Project, + name='pip', + slug='pip', + users=[self.owner], + privacy_level='public', + urlconf='subpath/$subproject/$version/$language/$filename' # Flipped + ) + self.subproject = get( + Project, + name='subproject', + slug='subproject', + users=[self.owner], + privacy_level='public', + main_language_project=None, + ) + self.relationship = get( + ProjectRelationship, + parent=self.pip, + child=self.subproject, + ) + + self.old_urlconf = get_urlconf() + sys.modules['fake_urlconf'] = self.pip.proxito_urlconf + set_urlconf('fake_urlconf') + + # TODO: Figure out why this is failing in travis + @pytest.mark.xfail(strict=True) + def test_middleware_urlconf_subproject(self): + resp = self.client.get('/subpath/subproject/testing/en/foodex.html', HTTP_HOST=self.domain) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp['X-Accel-Redirect'], + '/proxito/media/html/subproject/testing/foodex.html', + ) diff --git a/readthedocs/proxito/urls.py b/readthedocs/proxito/urls.py index e657edc0e7b..cd89aa754d7 100644 --- a/readthedocs/proxito/urls.py +++ b/readthedocs/proxito/urls.py @@ -50,7 +50,7 @@ DOC_PATH_PREFIX = getattr(settings, 'DOC_PATH_PREFIX', '') -urlpatterns = [ +proxied_urls = [ # Serve project downloads # /_/downloads//// url( @@ -89,7 +89,9 @@ ), include('readthedocs.api.v2.proxied_urls'), ), +] +core_urls = [ # Serve custom 404 pages url( r'^_proxito_404_(?P.*)$', @@ -98,6 +100,9 @@ ), url(r'robots\.txt$', ServeRobotsTXT.as_view(), name='robots_txt'), url(r'sitemap\.xml$', ServeSitemapXML.as_view(), name='sitemap_xml'), +] + +docs_urls = [ # # TODO: Support this? # (Sub)project `page` redirect @@ -158,6 +163,8 @@ ), ] +urlpatterns = proxied_urls + core_urls + docs_urls + # Use Django default error handlers to make things simpler handler404 = proxito_404_page_handler handler500 = defaults.server_error diff --git a/readthedocs/proxito/views/utils.py b/readthedocs/proxito/views/utils.py index 68ad8c419ee..21f25301b8f 100644 --- a/readthedocs/proxito/views/utils.py +++ b/readthedocs/proxito/views/utils.py @@ -29,7 +29,7 @@ def proxito_404_page_handler(request, exception=None, template_name='404.html'): Maze page. """ - if request.resolver_match.url_name != 'proxito_404_handler': + if request.resolver_match and request.resolver_match.url_name != 'proxito_404_handler': return fast_404(request, exception, template_name) resp = render(request, template_name) @@ -76,9 +76,15 @@ def _get_project_data_from_request( # Handle single version by grabbing the default version # We might have version_slug when we're serving a PR - if final_project.single_version and not version_slug: + if any([ + not version_slug and final_project.single_version, + not version_slug and project.urlconf + ]): version_slug = final_project.get_default_version() + if not lang_slug and project.urlconf: + lang_slug = final_project.language + # ``final_project`` is now the actual project we want to serve docs on, # accounting for: # * Project diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py index cb3106572ab..78be249aff5 100644 --- a/readthedocs/rtd_tests/tests/test_api.py +++ b/readthedocs/rtd_tests/tests/test_api.py @@ -790,6 +790,7 @@ def setUp(self): self.project = get( Project, build_queue=None, + external_builds_enabled=True, ) self.feature_flag = get( Feature, @@ -2254,6 +2255,7 @@ def test_webhook_build_another_branch(self, trigger_build): class APIVersionTests(TestCase): fixtures = ['eric', 'test_data'] + maxDiff = None # So we get an actual diff when it fails def test_get_version_by_id(self): """ @@ -2314,6 +2316,7 @@ def test_get_version_by_id(self): 'slug': 'pip', 'use_system_packages': False, 'users': [1], + 'urlconf': None, }, 'privacy_level': 'public', 'downloads': {}, diff --git a/readthedocs/rtd_tests/tests/test_resolver.py b/readthedocs/rtd_tests/tests/test_resolver.py index f53aad2460c..5e02790dbcc 100644 --- a/readthedocs/rtd_tests/tests/test_resolver.py +++ b/readthedocs/rtd_tests/tests/test_resolver.py @@ -176,6 +176,13 @@ def test_resolver_translation(self): url = resolve_path(project=self.translation, filename='index.html') self.assertEqual(url, '/ja/latest/index.html') + def test_resolver_urlconf(self): + url = resolve_path(project=self.translation, filename='index.html', urlconf='$version/$filename') + self.assertEqual(url, 'latest/index.html') + + def test_resolver_urlconf_extra(self): + url = resolve_path(project=self.translation, filename='index.html', urlconf='foo/bar/$version/$filename') + self.assertEqual(url, 'foo/bar/latest/index.html') class ResolverPathOverrideTests(ResolverBase): diff --git a/readthedocs/search/tests/test_proxied_api.py b/readthedocs/search/tests/test_proxied_api.py index d77d769d5d7..948ee298775 100644 --- a/readthedocs/search/tests/test_proxied_api.py +++ b/readthedocs/search/tests/test_proxied_api.py @@ -7,7 +7,8 @@ @pytest.mark.search class TestProxiedSearchAPI(BaseTestDocumentSearch): - host = 'pip.readthedocs.io' + # This project slug needs to exist in the ``all_projects`` fixture. + host = 'docs.readthedocs.io' @pytest.fixture(autouse=True) def setup_settings(self, settings): diff --git a/requirements/testing.txt b/requirements/testing.txt index 97adde92c7c..2c93585dfa5 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -2,9 +2,9 @@ -r local-docs-build.txt django-dynamic-fixture==3.1.0 -pytest==5.2.2 +pytest==5.4.2 pytest-custom-exit-code==0.3.0 -pytest-django==3.6.0 +pytest-django==3.8.0 pytest-xdist==1.30.0 pytest-cov==2.8.1 apipkg==1.5 diff --git a/setup.cfg b/setup.cfg index 534e38d8c67..bb02875fae1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = readthedocs -version = 5.1.0 +version = 5.1.1 license = MIT description = Read the Docs builds and hosts documentation author = Read the Docs, Inc