diff --git a/package-lock.json b/package-lock.json index a41f1567c9b..b6c4eed2520 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "readthedocs", - "version": "3.11.4", + "version": "5.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/readthedocs/analytics/signals.py b/readthedocs/analytics/signals.py index 097a5125606..fb95616c609 100644 --- a/readthedocs/analytics/signals.py +++ b/readthedocs/analytics/signals.py @@ -4,29 +4,41 @@ from django.utils import timezone from readthedocs.api.v2.signals import footer_response +from readthedocs.core.unresolver import unresolve_from_request from readthedocs.projects.models import Feature from .models import PageView @receiver(footer_response) -def increase_page_view_count(sender, **kwargs): +def increase_page_view_count(sender, *, request, context, absolute_uri, **kwargs): """Increase the page view count for the given project.""" - del sender # unused - context = kwargs['context'] + # unused + del sender + del kwargs project = context['project'] version = context['version'] - # footer_response sends an empty path for the index - path = context['path'] or '/' - - if project.has_feature(Feature.STORE_PAGEVIEWS): - page_view, _ = PageView.objects.get_or_create( - project=project, - version=version, - path=path, - date=timezone.now().date(), - ) - PageView.objects.filter(pk=page_view.pk).update( - view_count=F('view_count') + 1 - ) + + if not absolute_uri or not project.has_feature(Feature.STORE_PAGEVIEWS): + return + + unresolved = unresolve_from_request(request, absolute_uri) + if not unresolved: + return + + path = unresolved.filename + + fields = dict( + project=project, + version=version, + path=path, + date=timezone.now().date(), + ) + + page_view = PageView.objects.filter(**fields).first() + if page_view: + page_view.view_count = F('view_count') + 1 + page_view.save(update_fields=['view_count']) + else: + PageView.objects.create(**fields, view_count=1) diff --git a/readthedocs/analytics/tests.py b/readthedocs/analytics/tests.py index e8eae5c5fe1..7b3811561c0 100644 --- a/readthedocs/analytics/tests.py +++ b/readthedocs/analytics/tests.py @@ -1,19 +1,16 @@ from unittest import mock -from django_dynamic_fixture import get -from django.test import TestCase, RequestFactory +import pytest +from django.test import RequestFactory, TestCase, override_settings +from django.urls import reverse from django.utils import timezone +from django_dynamic_fixture import get from readthedocs.builds.models import Version -from readthedocs.projects.models import Project, Feature +from readthedocs.projects.models import Feature, Project from .models import PageView -from .signals import increase_page_view_count -from .utils import ( - anonymize_ip_address, - anonymize_user_agent, - get_client_ip, -) +from .utils import anonymize_ip_address, anonymize_user_agent, get_client_ip class UtilsTests(TestCase): @@ -97,31 +94,35 @@ def test_get_client_ip_with_remote_addr(self): self.assertEqual(client_ip, '203.0.113.195') -class AnalyticsTasksTests(TestCase): - def test_increase_page_view_count(self): - project = get( +@pytest.mark.proxito +@override_settings(PUBLIC_DOMAIN='readthedocs.io') +class AnalyticsPageViewsTests(TestCase): + + def setUp(self): + self.project = get( Project, - slug='project-1', + slug='pip', + ) + self.version = get(Version, slug='1.8', project=self.project) + self.absolute_uri = f'https://{self.project.slug}.readthedocs.io/en/latest/index.html' + self.host = f'{self.project.slug}.readthedocs.io' + self.url = ( + reverse('footer_html') + + f'?project={self.project.slug}&version={self.version.slug}&page=index&docroot=/docs/' + + f'&absolute_uri={self.absolute_uri}' ) - version = get(Version, slug='1.8', project=project) - path = "index" - today = timezone.now() - tomorrow = timezone.now() + timezone.timedelta(days=1) - yesterday = timezone.now() - timezone.timedelta(days=1) + self.today = timezone.now() + self.tomorrow = timezone.now() + timezone.timedelta(days=1) + self.yesterday = timezone.now() - timezone.timedelta(days=1) + def test_increase_page_view_count(self): assert ( PageView.objects.all().count() == 0 ), 'There\'s no PageView object created yet.' - context = { - "project": project, - "version": version, - "path": path, - } - # Without the feature flag, no PageView is created - increase_page_view_count(None, context=context) + self.client.get(self.url, HTTP_HOST=self.host) assert ( PageView.objects.all().count() == 0 ) @@ -129,50 +130,55 @@ def test_increase_page_view_count(self): feature, _ = Feature.objects.get_or_create( feature_id=Feature.STORE_PAGEVIEWS, ) - project.feature_set.add(feature) + self.project.feature_set.add(feature) # testing for yesterday with mock.patch('readthedocs.analytics.tasks.timezone.now') as mocked_timezone: - mocked_timezone.return_value = yesterday + mocked_timezone.return_value = self.yesterday - increase_page_view_count(None, context=context) + self.client.get(self.url, HTTP_HOST=self.host) assert ( PageView.objects.all().count() == 1 - ), f'PageView object for path \'{path}\' is created' + ), f'PageView object for path \'{self.absolute_uri}\' is created' assert ( PageView.objects.all().first().view_count == 1 ), '\'index\' has 1 view' - increase_page_view_count(None, context=context) + self.client.get(self.url, HTTP_HOST=self.host) assert ( PageView.objects.all().count() == 1 - ), f'PageView object for path \'{path}\' is already created' + ), f'PageView object for path \'{self.absolute_uri}\' is already created' + assert PageView.objects.filter(path='index.html').count() == 1 assert ( PageView.objects.all().first().view_count == 2 - ), f'\'{path}\' has 2 views now' + ), f'\'{self.absolute_uri}\' has 2 views now' # testing for today with mock.patch('readthedocs.analytics.tasks.timezone.now') as mocked_timezone: - mocked_timezone.return_value = today - increase_page_view_count(None, context=context) + mocked_timezone.return_value = self.today + + self.client.get(self.url, HTTP_HOST=self.host) assert ( PageView.objects.all().count() == 2 - ), f'PageView object for path \'{path}\' is created for two days (yesterday and today)' + ), f'PageView object for path \'{self.absolute_uri}\' is created for two days (yesterday and today)' + assert PageView.objects.filter(path='index.html').count() == 2 assert ( PageView.objects.all().order_by('-date').first().view_count == 1 - ), f'\'{path}\' has 1 view today' + ), f'\'{self.absolute_uri}\' has 1 view today' # testing for tomorrow with mock.patch('readthedocs.analytics.tasks.timezone.now') as mocked_timezone: - mocked_timezone.return_value = tomorrow - increase_page_view_count(None, context=context) + mocked_timezone.return_value = self.tomorrow + + self.client.get(self.url, HTTP_HOST=self.host) assert ( PageView.objects.all().count() == 3 - ), f'PageView object for path \'{path}\' is created for three days (yesterday, today & tomorrow)' + ), f'PageView object for path \'{self.absolute_uri}\' is created for three days (yesterday, today & tomorrow)' + assert PageView.objects.filter(path='index.html').count() == 3 assert ( PageView.objects.all().order_by('-date').first().view_count == 1 - ), f'\'{path}\' has 1 view tomorrow' + ), f'\'{self.absolute_uri}\' has 1 view tomorrow' diff --git a/readthedocs/api/v2/signals.py b/readthedocs/api/v2/signals.py index 419e78f43f5..dfb06d77540 100644 --- a/readthedocs/api/v2/signals.py +++ b/readthedocs/api/v2/signals.py @@ -4,5 +4,5 @@ footer_response = django.dispatch.Signal( - providing_args=['request', 'context', 'response_data'], + providing_args=['request', 'context', 'response_data', 'absolute_uri'], ) diff --git a/readthedocs/api/v2/views/footer_views.py b/readthedocs/api/v2/views/footer_views.py index 3ba178f16fc..759244a4ee1 100644 --- a/readthedocs/api/v2/views/footer_views.py +++ b/readthedocs/api/v2/views/footer_views.py @@ -84,6 +84,19 @@ class BaseFooterHTML(APIView): """ Render and return footer markup. + Query parameters: + + - project + - version + - page: Sphinx's page name, used for path operations, + like change between languages (deprecated in favor of ``absolute_uri``). + - absolute_uri: Full path with domain, used for path operations. + - theme: Used to decide how to integrate the flyout menu. + - docroot: Path where all the source documents are. + Used to build the ``edit_on`` URL. + - source_suffix: Suffix from the source document. + Used to build the ``edit_on`` URL. + .. note:: The methods `_get_project` and `_get_version` @@ -236,7 +249,8 @@ def get(self, request, format=None): sender=None, request=request, context=context, - resp_data=resp_data, + response_data=resp_data, + absolute_uri=self.request.GET.get('absolute_uri'), ) return Response(resp_data) diff --git a/readthedocs/core/static-src/core/js/doc-embed/footer.js b/readthedocs/core/static-src/core/js/doc-embed/footer.js index 6cd7dfe5783..b7fc76f3ab0 100644 --- a/readthedocs/core/static-src/core/js/doc-embed/footer.js +++ b/readthedocs/core/static-src/core/js/doc-embed/footer.js @@ -44,6 +44,7 @@ function init() { project: rtd['project'], version: rtd['version'], page: rtd['page'], + absolute_uri: window.location.href, theme: rtd.get_theme_name(), format: "jsonp", }; diff --git a/readthedocs/core/static/core/js/readthedocs-doc-embed.js b/readthedocs/core/static/core/js/readthedocs-doc-embed.js index 52bb54d2b25..bff2fa451b3 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(a,s,l){function c(t,e){if(!s[t]){if(!a[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(d)return d(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var r=s[t]={exports:{}};a[t][0].call(r.exports,function(e){return c(a[t][1][e]||e)},r,r.exports,o,a,s,l)}return s[t].exports}for(var d="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 y(e){return e.replace(h,'"')}function _(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=d.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||a).process(i))}return i=E(i)},i.escapeHtml=s,i.escapeQuote=b,i.unescapeQuote=y,i.escapeHtmlEntities=_,i.escapeDangerHtml5Entities=x,i.clearNonPrintableCharacter=k,i.friendlyAttrValue=T,i.escapeAttrValue=E,i.onIgnoreTagStripAll=function(){return""},i.StripTagBody=function(o,a){"function"!=typeof a&&(a=function(){});var s=!Array.isArray(o),l=[],c=!1;return{onIgnoreTag:function(e,t,i){if(function(e){return s||-1!==d.indexOf(o,e)}(e)){if(i.isClosing){var n="[/removed]",r=i.position+n.length;return l.push([!1!==c?c:i.position,r]),c=!1,n}return c=c||i.position,"[removed]"}return a(e,t,i)},remove:function(t){var i="",n=0;return d.forEach(l,function(e){i+=t.slice(n,e[0]),n=e[1]}),i+=t.slice(n)}}},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=a,i.getDefaultCSSWhiteList=r},{"./util":5,cssfilter:10}],3:[function(e,t,i){var n=e("./default"),r=e("./parser"),o=e("./xss");for(var a in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,n)i[a]=n[a];for(var a in r)i[a]=r[a];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var d=e("./util");function h(e){var t=d.spaceIndex(e);if(-1===t)var i=e.slice(1,-1);else i=e.slice(1,t+1);return"/"===(i=d.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)),d=h(c=e.slice(o,s+1)),n+=t(o,n.length,d,c,"";var s=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=d[r],c=w(s.html,function(e,t){var i,n=-1!==b.indexOf(l,e);return y(i=p(r,e,t,n))?n?(t=g(r,e,t,v))?e+'="'+t+'"':e:y(i=f(r,e,t,n))?void 0:i:i});i="<"+r;return c&&(i+=" "+c),s.closing&&(i+=" /"),i+=">"}return y(o=h(r,i,a))?m(i):o},m);return i&&(n=i.remove(n)),n},t.exports=s},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var n,r;n=this,r=function(){var T=!0;function a(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=s;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(n=a(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 s.test=function(e){for(var t=0;t");if(a.append($("

").append($("",{href:r.path,text:r.title}))),r.project!==b){var s="(from project "+r.project+")";a.append($("",{text:s}))}for(var l=0;lC&&(h=h.substr(0,C)+" ...");var p=[h];if(c.highlights.title.length&&(u=c.highlights.title[0]),c.highlights.content.length){var f=c.highlights.content;p=[];for(var g=0;g/g,"").replace(/<\/span>/g,""),a.append($("

").append($("",{href:d}).html(u)));for(var m=0;m/g,"").replace(/<\/span>/g,""),a.append($("

").html(v))}i.append(a)}}}}else console.log("Read the Docs search returned 0 result. Falling back to MkDocs search."),w()}).fail(function(e){console.log("Read the Docs search failed. Falling back to MkDocs search."),w()}),$.ajax({url:t.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()})}function e(){var e=document.getElementById("mkdocs-search-query");e&&e.addEventListener("keyup",n);var t=window.getSearchTermFromLocation();t&&(e.value=t,n())}var b=i.project,r=i.version,o=i.language||"en";$(document).ready(function(){window.doSearchFallback=window.doSearch,window.doSearch=n,(window.initSearch=e)()})}t.exports={init:function(){var e=n.get();e.is_sphinx_builder()?function(t){var A=t.project,i=t.version,r=t.language||"en";if("undefined"!=typeof Search&&A&&i)if(t.features&&t.features.docsearch_disabled)console.log("Server side search is disabled.");else{var e=Search.query;Search.query_fallback=e,Search.query=function(S){var n=$.Deferred(),e=document.createElement("a");e.href=t.proxied_api_host+"/api/v2/search/",e.search="?q="+$.urlencode(S)+"&project="+A+"&version="+i+"&language="+r,n.then(function(e){var t=e.results||[];if(t.length){for(var i=0;i'),a=n.title;n.highlights.title.length&&(a=O(n.highlights.title[0]));var s=n.path+"?highlight="+$.urlencode(S),l=$("",{href:s});if(l.html(a),l.find("span").addClass("highlighted"),o.append(l),n.project!==A){var c=" (from project "+n.project+")",d=$("",{text:c});o.append(d)}for(var u=0;u');if("section"===h.type){var f=h,g=f.title,m=s+"#"+f.id,v=[f.content.substr(0,C)+" ..."];if(f.highlights.title.length&&(g=O(f.highlights.title[0])),f.highlights.content.length){var w=f.highlights.content;v=[];for(var b=0;b<%= section_subtitle %><% for (var i = 0; i < section_content.length; ++i) { %>

<%= section_content[i] %>
<% } %>',{section_subtitle_link:m,section_subtitle:g,section_content:v})}if("domain"===h.type){var y=h,x=y.role,k=s+"#"+y.id,T=y.name,E="";""!==y.content&&(E=y.content.substr(0,C)+" ..."),y.highlights.content.length&&(E="... "+O(y.highlights.content[0])+" ..."),y.highlights.name.length&&(T=O(y.highlights.name[0])),M(p,'
<%= domain_content %>
',{domain_subtitle_link:k,domain_subtitle:"["+x+"]: "+T,domain_content:E})}p.find("span").addClass("highlighted"),o.append(p),u"))}Search.output.append(o),o.slideDown(5)}Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length))}else console.log("Read the Docs search failed. Falling back to Sphinx search."),Search.query_fallback(S)}).fail(function(e){console.debug("Read the Docs search failed. Falling back to Sphinx search."),Search.query_fallback(S)}).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()})}(e):e.features&&!e.features.docsearch_disabled?r(e):console.log("Server side search is disabled.")}}},{"./../../../../../../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,c=e("./constants"),d=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=c.PROMO_TYPES.LEFTNAV,r=c.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=c.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=c.PROMO_TYPES.FOOTER,r=c.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=c.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=c.PROMO_TYPES.FIXED_FOOTER,i=c.DEFAULT_PROMO_PRIORITY;return n&&n.mobile&&(i=c.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===c.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=[],a=[p,h,f];if(l=d.get(),t=function(){var e,t="rtd-"+(Math.random()+1).toString(36).substring(4),i=c.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||c.DEFAULT_PROMO_PRIORITY),!0;else{if(!l.show_promo())return;for(var s=0;s").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")),a=e("./doc-embed/search");$.extend(e("verge")),$(document).ready(function(){r.init(),o.init(),a.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(a,s,l){function c(t,e){if(!s[t]){if(!a[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(d)return d(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var r=s[t]={exports:{}};a[t][0].call(r.exports,function(e){return c(a[t][1][e]||e)},r,r.exports,o,a,s,l)}return s[t].exports}for(var d="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=d.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||a).process(i))}return i=E(i)},i.escapeHtml=s,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,a){"function"!=typeof a&&(a=function(){});var s=!Array.isArray(o),l=[],c=!1;return{onIgnoreTag:function(e,t,i){if(function(e){return s||-1!==d.indexOf(o,e)}(e)){if(i.isClosing){var n="[/removed]",r=i.position+n.length;return l.push([!1!==c?c:i.position,r]),c=!1,n}return c=c||i.position,"[removed]"}return a(e,t,i)},remove:function(t){var i="",n=0;return d.forEach(l,function(e){i+=t.slice(n,e[0]),n=e[1]}),i+=t.slice(n)}}},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=a,i.getDefaultCSSWhiteList=r},{"./util":5,cssfilter:10}],3:[function(e,t,i){var n=e("./default"),r=e("./parser"),o=e("./xss");for(var a in(i=t.exports=function(e,t){return new o(t).process(e)}).FilterXSS=o,n)i[a]=n[a];for(var a in r)i[a]=r[a];"undefined"!=typeof window&&(window.filterXSS=t.exports)},{"./default":2,"./parser":4,"./xss":6}],4:[function(e,t,i){var d=e("./util");function h(e){var t=d.spaceIndex(e);if(-1===t)var i=e.slice(1,-1);else i=e.slice(1,t+1);return"/"===(i=d.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)),d=h(c=e.slice(o,s+1)),n+=t(o,n.length,d,c,"";var s=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=d[r],c=w(s.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 c&&(i+=" "+c),s.closing&&(i+=" /"),i+=">"}return _(o=h(r,i,a))?m(i):o},m);return i&&(n=i.remove(n)),n},t.exports=s},{"./default":2,"./parser":4,"./util":5,cssfilter:10}],7:[function(e,t,i){var n,r;n=this,r=function(){var T=!0;function a(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=s;"string"==typeof t&&(i=t,t=void 0),void 0===t&&(t=!1),i&&(n=a(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 s.test=function(e){for(var t=0;t");if(a.append($("

").append($("",{href:r.path,text:r.title}))),r.project!==b){var s="(from project "+r.project+")";a.append($("",{text:s}))}for(var l=0;lC&&(h=h.substr(0,C)+" ...");var p=[h];if(c.highlights.title.length&&(u=c.highlights.title[0]),c.highlights.content.length){var f=c.highlights.content;p=[];for(var g=0;g/g,"").replace(/<\/span>/g,""),a.append($("

").append($("",{href:d}).html(u)));for(var m=0;m/g,"").replace(/<\/span>/g,""),a.append($("

").html(v))}i.append(a)}}}}else console.log("Read the Docs search returned 0 result. Falling back to MkDocs search."),w()}).fail(function(e){console.log("Read the Docs search failed. Falling back to MkDocs search."),w()}),$.ajax({url:t.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()})}function e(){var e=document.getElementById("mkdocs-search-query");e&&e.addEventListener("keyup",n);var t=window.getSearchTermFromLocation();t&&(e.value=t,n())}var b=i.project,r=i.version,o=i.language||"en";$(document).ready(function(){window.doSearchFallback=window.doSearch,window.doSearch=n,(window.initSearch=e)()})}t.exports={init:function(){var e=n.get();e.is_sphinx_builder()?function(t){var A=t.project,i=t.version,r=t.language||"en";if("undefined"!=typeof Search&&A&&i)if(t.features&&t.features.docsearch_disabled)console.log("Server side search is disabled.");else{var e=Search.query;Search.query_fallback=e,Search.query=function(S){var n=$.Deferred(),e=document.createElement("a");e.href=t.proxied_api_host+"/api/v2/search/",e.search="?q="+$.urlencode(S)+"&project="+A+"&version="+i+"&language="+r,n.then(function(e){var t=e.results||[];if(t.length){for(var i=0;i'),a=n.title;n.highlights.title.length&&(a=O(n.highlights.title[0]));var s=n.path+"?highlight="+$.urlencode(S),l=$("",{href:s});if(l.html(a),l.find("span").addClass("highlighted"),o.append(l),n.project!==A){var c=" (from project "+n.project+")",d=$("",{text:c});o.append(d)}for(var u=0;u');if("section"===h.type){var f=h,g=f.title,m=s+"#"+f.id,v=[f.content.substr(0,C)+" ..."];if(f.highlights.title.length&&(g=O(f.highlights.title[0])),f.highlights.content.length){var w=f.highlights.content;v=[];for(var b=0;b<%= section_subtitle %>

<% for (var i = 0; i < section_content.length; ++i) { %>
<%= section_content[i] %>
<% } %>',{section_subtitle_link:m,section_subtitle:g,section_content:v})}if("domain"===h.type){var y=h,x=y.role,k=s+"#"+y.id,T=y.name,E="";""!==y.content&&(E=y.content.substr(0,C)+" ..."),y.highlights.content.length&&(E="... "+O(y.highlights.content[0])+" ..."),y.highlights.name.length&&(T=O(y.highlights.name[0])),M(p,'
<%= domain_content %>
',{domain_subtitle_link:k,domain_subtitle:"["+x+"]: "+T,domain_content:E})}p.find("span").addClass("highlighted"),o.append(p),u
"))}Search.output.append(o),o.slideDown(5)}Search.status.text(_("Search finished, found %s page(s) matching the search query.").replace("%s",t.length))}else console.log("Read the Docs search failed. Falling back to Sphinx search."),Search.query_fallback(S)}).fail(function(e){console.debug("Read the Docs search failed. Falling back to Sphinx search."),Search.query_fallback(S)}).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()})}(e):e.features&&!e.features.docsearch_disabled?r(e):console.log("Server side search is disabled.")}}},{"./../../../../../../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,c=e("./constants"),d=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=c.PROMO_TYPES.LEFTNAV,r=c.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=c.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=c.PROMO_TYPES.FOOTER,r=c.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=c.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=c.PROMO_TYPES.FIXED_FOOTER,i=c.DEFAULT_PROMO_PRIORITY;return n&&n.mobile&&(i=c.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===c.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=[],a=[p,h,f];if(l=d.get(),t=function(){var e,t="rtd-"+(Math.random()+1).toString(36).substring(4),i=c.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||c.DEFAULT_PROMO_PRIORITY),!0;else{if(!l.show_promo())return;for(var s=0;s").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")),a=e("./doc-embed/search");$.extend(e("verge")),$(document).ready(function(){r.init(),o.init(),a.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/core/unresolver.py b/readthedocs/core/unresolver.py index bd5a6da9f14..38677f94828 100644 --- a/readthedocs/core/unresolver.py +++ b/readthedocs/core/unresolver.py @@ -1,14 +1,14 @@ import logging -from urllib.parse import urlparse from collections import namedtuple +from urllib.parse import urlparse -from django.urls import resolve as url_resolve from django.test.client import RequestFactory +from django.urls import resolve as url_resolve from readthedocs.core.utils.extend import SettingsOverrideObject from readthedocs.proxito.middleware import map_host_to_project_slug -from readthedocs.proxito.views.utils import _get_project_data_from_request from readthedocs.proxito.views.mixins import ServeDocsMixin +from readthedocs.proxito.views.utils import _get_project_data_from_request log = logging.getLogger(__name__) @@ -27,17 +27,35 @@ def unresolve(self, url): """ parsed = urlparse(url) domain = parsed.netloc.split(':', 1)[0] - path = parsed.path # TODO: Make this not depend on the request object, # but instead move all this logic here working on strings. - request = RequestFactory().get(path=path, HTTP_HOST=domain) - project_slug = request.host_project_slug = map_host_to_project_slug(request) + request = RequestFactory().get(path=parsed.path, HTTP_HOST=domain) + project_slug = map_host_to_project_slug(request) # Handle returning a response if hasattr(project_slug, 'status_code'): return None + request.host_project_slug = request.slug = project_slug + return self.unresolve_from_request(request, url) + + def unresolve_from_request(self, request, path): + """ + Unresolve using a request. + + ``path`` can be a full URL, but the domain will be ignored, + since that information is already in the request object. + + None is returned if the request isn't valid. + """ + parsed = urlparse(path) + path = parsed.path + project_slug = getattr(request, 'host_project_slug', None) + + if not project_slug: + return None + _, __, kwargs = url_resolve( path, urlconf='readthedocs.proxito.urls', @@ -63,8 +81,8 @@ def unresolve(self, url): log.info( 'Unresolver parsed: ' - 'url=%s project=%s lang_slug=%s version_slug=%s filename=%s', - url, final_project.slug, lang_slug, version_slug, filename + 'project=%s lang_slug=%s version_slug=%s filename=%s', + final_project.slug, lang_slug, version_slug, filename ) return UnresolvedObject(final_project, lang_slug, version_slug, filename, parsed.fragment) @@ -76,3 +94,4 @@ class Unresolver(SettingsOverrideObject): unresolver = Unresolver() unresolve = unresolver.unresolve +unresolve_from_request = unresolver.unresolve_from_request diff --git a/readthedocs/rtd_tests/tests/test_footer.py b/readthedocs/rtd_tests/tests/test_footer.py index 87b3974d9ae..0d6bdb78e2d 100644 --- a/readthedocs/rtd_tests/tests/test_footer.py +++ b/readthedocs/rtd_tests/tests/test_footer.py @@ -1,11 +1,11 @@ from unittest import mock +import pytest from django.contrib.sessions.backends.base import SessionBase -from django.test import TestCase -from django.test.utils import override_settings +from django.test import TestCase, override_settings from django.urls import reverse from django_dynamic_fixture import get -from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework.test import APIRequestFactory from readthedocs.api.v2.views.footer_views import ( FooterHTML, @@ -15,7 +15,7 @@ from readthedocs.builds.models import Version from readthedocs.core.middleware import ReadTheDocsSessionMiddleware from readthedocs.projects.constants import PUBLIC -from readthedocs.projects.models import Project +from readthedocs.projects.models import Feature, Project class BaseTestFooterHTML: @@ -422,33 +422,56 @@ def test_highest_version_without_tags(self): self.assertDictEqual(valid_data, returned_data) -class TestFooterPerformance(APITestCase): - fixtures = ['test_data', 'eric'] - url = '/api/v2/footer_html/?project=pip&version=latest&page=index&docroot=/' - factory = APIRequestFactory() - +@pytest.mark.proxito +@override_settings(PUBLIC_DOMAIN='readthedocs.io') +class TestFooterPerformance(TestCase): # The expected number of queries for generating the footer # This shouldn't increase unless we modify the footer API - EXPECTED_QUERIES = 14 + EXPECTED_QUERIES = 18 def setUp(self): - self.pip = Project.objects.get(slug='pip') - self.pip.show_version_warning = True - self.pip.save() - self.pip.versions.update(built=True) + self.pip = get( + Project, + slug='pip', + repo='https://github.com/rtfd/readthedocs.org', + privacy_level=PUBLIC, + show_version_warning=True, + main_language_project=None, + ) + self.pip.versions.create( + verbose_name='0.8.1', + identifier='0.8.1', + type=TAG, + ) + self.pip.versions.update(privacy_level=PUBLIC, built=True, active=True) + self.latest = self.pip.versions.get(slug=LATEST) - def render(self): - request = self.factory.get(self.url) - response = FooterHTML.as_view()(request) - response.render() - return response + self.url = ( + reverse('footer_html') + + f'?project={self.pip.slug}&version={self.latest.slug}&page=index&docroot=/docs/' + + '&absolute_uri=https://pip.readthedocs.io/en/latest/index.html' + ) + self.host = 'pip.readthedocs.io' + + # Run tests with all available features + # that can increase the number of queries. + feature, _ = Feature.objects.get_or_create( + feature_id=Feature.STORE_PAGEVIEWS, + ) + self.pip.feature_set.add(feature) def test_version_queries(self): - # The number of Versions shouldn't impact the number of queries with self.assertNumQueries(self.EXPECTED_QUERIES): - response = self.render() + response = self.client.get(self.url, HTTP_HOST=self.host) self.assertContains(response, '0.8.1') + # Second time we don't create a new page view, + # this shouldn't impact the number of queries. + with self.assertNumQueries(self.EXPECTED_QUERIES): + response = self.client.get(self.url, HTTP_HOST=self.host) + self.assertContains(response, '0.8.1') + + # The number of Versions shouldn't impact the number of queries for patch in range(3): identifier = '0.99.{}'.format(patch) self.pip.versions.create( @@ -460,16 +483,17 @@ def test_version_queries(self): ) with self.assertNumQueries(self.EXPECTED_QUERIES): - response = self.render() + response = self.client.get(self.url, HTTP_HOST=self.host) self.assertContains(response, '0.99.0') def test_domain_queries(self): - # Setting up a custom domain shouldn't impact the number of queries + domain = 'docs.foobar.com' self.pip.domains.create( - domain='http://docs.foobar.com', + domain=f'http://{domain}', canonical=True, ) - with self.assertNumQueries(self.EXPECTED_QUERIES): - response = self.render() - self.assertContains(response, 'docs.foobar.com') + # Setting up a custom domain increases only one query. + with self.assertNumQueries(self.EXPECTED_QUERIES + 1): + response = self.client.get(self.url, HTTP_HOST=domain) + self.assertContains(response, domain)