├── include_by_ajax ├── tests │ ├── __init__.py │ └── test_templatetags.py ├── templatetags │ ├── __init__.py │ └── include_by_ajax_tags.py ├── __init__.py ├── templates │ └── include_by_ajax │ │ └── includes │ │ └── placeholder.html ├── static │ └── include_by_ajax │ │ └── js │ │ ├── include_by_ajax.min.js │ │ └── include_by_ajax.js └── apps.py ├── MANIFEST.in ├── .gitignore ├── AUTHORS.md ├── setup.cfg ├── LICENSE.md ├── setup.py ├── CHANGELOG.md └── README.md /include_by_ajax/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft include_by_ajax 2 | -------------------------------------------------------------------------------- /include_by_ajax/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | .vscode 4 | .DS_Store 5 | build 6 | dist 7 | django_include_by_ajax.egg-info 8 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | - Aidas Bendoraitis (archatas) 4 | 5 | # Contributors 6 | 7 | - Zach Galant (zgalant) 8 | - Martin Borst (martinborst) 9 | - Andreu Vallbona Plazas (avallbona) -------------------------------------------------------------------------------- /include_by_ajax/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | __version__ = "3.0.2" 5 | default_app_config = "include_by_ajax.apps.IncludeByAjaxConfig" 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.0.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:include_by_ajax/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | 14 | [bumpversion:file:CHANGELOG.md] 15 | search = 16 | [Unreleased] 17 | ------------ 18 | replace = 19 | [Unreleased] 20 | ------------ 21 | 22 | [{new_version}] - {utcnow:%%Y-%%m-%%d} 23 | -------------------- 24 | 25 | [bdist_wheel] 26 | universal = 1 27 | -------------------------------------------------------------------------------- /include_by_ajax/templates/include_by_ajax/includes/placeholder.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% if include_by_ajax_no_placeholder_wrapping %} 3 | {% if include_by_ajax_full_render %} 4 | {% include template_name %} 5 | {% endif %} 6 | {% else %} 7 |
8 | {% if include_by_ajax_full_render %} 9 | {% include template_name %} 10 | {% elif placeholder_template_name %} 11 | {% include placeholder_template_name %} 12 | {% endif %} 13 |
14 | {% endif %} 15 | {% endspaceless %} 16 | -------------------------------------------------------------------------------- /include_by_ajax/static/include_by_ajax/js/include_by_ajax.min.js: -------------------------------------------------------------------------------- 1 | jQuery((function(e){let a=e(".js-ajax-placeholder");if(!a.length)return;let l=location.href.replace(/#.*/,"");-1===l.indexOf("?")?l+="?include_by_ajax_full_render=1":l+="&include_by_ajax_full_render=1",e.ajax({async:!0,url:l,dataType:"html"}).done((function(l,n){let t=[];e("
").append(e.parseHTML(l,document,!0)).find(".js-ajax-placeholder>*").each((function(l,n){let i=e(n);i.find("script").each((function(){t.push(e(this)),e(this).remove()})),e(a[l]).replaceWith(i)})),function a(){let l=t.shift();if(l){let n=l.attr("src");n?e.ajax({async:!0,url:n,dataType:"script"}).done((function(e,l){a()})).fail((function(e,l,n){a()})):(e.globalEval(l.html()),a())}else e(document).trigger("include_by_ajax_all_loaded")}()}))})); -------------------------------------------------------------------------------- /include_by_ajax/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | 6 | from django.apps import AppConfig 7 | from django.utils.translation import gettext_lazy as _ 8 | from django.conf import settings 9 | 10 | 11 | class IncludeByAjaxConfig(AppConfig): 12 | name = "include_by_ajax" 13 | verbose_name = _("Include by Ajax") 14 | 15 | WEB_CRAWLERS_WITHOUT_JS = getattr( 16 | settings, 17 | "INCLUDE_BY_AJAX_WEB_CRAWLERS", 18 | ( 19 | "Bingbot", # Bing bot 20 | "Slurp", # Yahoo! bot 21 | "DuckDuckBot", # Duck Duck Go bot 22 | "Baiduspider", # Baidu bot 23 | "YandexBot", # Yandex bot 24 | "Sogou", # Sogou bot 25 | "Exabot", # Exalead bot 26 | "ia_archiver", # Alexa bot 27 | ), 28 | ) 29 | web_crawler_pattern = re.compile( 30 | "|".join(sorted(WEB_CRAWLERS_WITHOUT_JS, reverse=True)), flags=re.IGNORECASE 31 | ) 32 | 33 | def ready(self): 34 | pass 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2018 Aidas Bendoraitis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /include_by_ajax/templatetags/include_by_ajax_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from __future__ import unicode_literals 3 | from django.apps import apps 4 | from django import template 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.inclusion_tag("include_by_ajax/includes/placeholder.html", takes_context=True) 10 | def include_by_ajax(context, template_name, placeholder_template_name=None): 11 | # get the app configuration 12 | app_config = apps.get_app_config("include_by_ajax") 13 | # get User-Agent 14 | request = context["request"] 15 | user_agent = request.META.get("HTTP_USER_AGENT") or "" 16 | # check if the current request is coming from a web crawler 17 | is_web_crawler = bool(app_config.web_crawler_pattern.search(user_agent)) 18 | # in case of web crawler or an Ajax call we'll do full render 19 | context["include_by_ajax_full_render"] = bool( 20 | is_web_crawler 21 | or request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" 22 | and request.GET.get("include_by_ajax_full_render") 23 | ) 24 | # in case of web crawler, the placeholder shouldn't be wrapped with a
25 | context["include_by_ajax_no_placeholder_wrapping"] = is_web_crawler 26 | # pass down the template paths 27 | context["template_name"] = template_name 28 | context["placeholder_template_name"] = placeholder_template_name 29 | return context 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | 7 | with open("README.md") as readme_file: 8 | readme = readme_file.read() 9 | 10 | requirements = [ 11 | "Django>=2.2", 12 | ] 13 | 14 | setup( 15 | author="Aidas Bendoraitis", 16 | author_email="aidasbend@yahoo.com", 17 | classifiers=[ 18 | "Development Status :: 5 - Production/Stable", 19 | "Framework :: Django", 20 | "Framework :: Django :: 2.2", 21 | "Framework :: Django :: 3.0", 22 | "Framework :: Django :: 3.1", 23 | "Framework :: Django :: 3.2", 24 | "Framework :: Django :: 4.0", 25 | "Framework :: Django :: 4.1", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | "Natural Language :: English", 29 | "Programming Language :: JavaScript", 30 | "Programming Language :: Python :: 2", 31 | "Programming Language :: Python :: 2.7", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | ], 42 | description="A Django App Providing the `{% include_by_ajax %}` Template Tag", 43 | install_requires=requirements, 44 | license="MIT license", 45 | long_description=readme, 46 | long_description_content_type="text/markdown", 47 | include_package_data=True, 48 | keywords="django_include_by_ajax", 49 | name="django-include-by-ajax", 50 | packages=find_packages(include=["include_by_ajax"]), 51 | url="https://github.com/archatas/django-include-by-ajax", 52 | version="3.0.2", 53 | zip_safe=False, 54 | ) 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | [Unreleased] 8 | ------------ 9 | 10 | [3.0.2] - 2022-10-28 11 | -------------------- 12 | 13 | ### Added 14 | 15 | - Django 3.1 support 16 | - Django 3.2 support 17 | - Django 4.0 support 18 | - Django 4.1 support 19 | 20 | ### Changed 21 | 22 | - All requests by JavaScript are done asynchronously. 23 | - Requests for Googlebot are not treated separately, because it can handle JavaScript. 24 | 25 | ### Removed 26 | 27 | - Django 1.8 support 28 | - Django 1.11 support 29 | - Django 2.0 support 30 | - Django 2.1 support 31 | 32 | [2.0.0] - 2019-12-03 33 | -------------------- 34 | 35 | ### Added 36 | 37 | - Django 3.0 support 38 | - CHANGELOG 39 | 40 | [1.1.0] - 2019-12-03 41 | -------------------- 42 | 43 | ### Added 44 | 45 | - Badge for PyPI 46 | 47 | ### Changed 48 | 49 | - README 50 | 51 | ### Fixed 52 | 53 | - Support for IE 11 54 | 55 | [1.0.0] - 2019-04-16 56 | -------------------- 57 | 58 | ### Added 59 | 60 | - Django 2.2 support 61 | - Execute JavaScript in the uploaded blocks 62 | - Minified JavaScript version 63 | 64 | ### Changed 65 | 66 | - README 67 | 68 | [0.5.0] - 2018-12-11 69 | -------------------- 70 | 71 | ### Added 72 | 73 | - Optional placeholder template 74 | 75 | ### Changed 76 | 77 | - README 78 | 79 | [0.4.0] - 2018-10-28 80 | -------------------- 81 | 82 | ### Added 83 | 84 | - Web crawlers load the full content without JavaScript 85 | - Unit tests 86 | 87 | ### Changed 88 | 89 | - CSS classes that are manipulated by JavaScript prefixed with "js-". 90 | 91 | [0.3.0] - 2018-09-11 92 | -------------------- 93 | 94 | ### Changed 95 | 96 | - README 97 | 98 | ### Fixed 99 | 100 | - A bug with '#' in the URL fixed. 101 | 102 | [0.2.0] - 2018-08-12 103 | -------------------- 104 | 105 | ### Added 106 | 107 | - Initial prototype 108 | - README 109 | 110 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /include_by_ajax/static/include_by_ajax/js/include_by_ajax.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true, unused:false */ 2 | jQuery(function($) { 3 | // 1. Check if there are placeholders 4 | let $placeholders = $('.js-ajax-placeholder'); 5 | let placeholder_count = $placeholders.length; 6 | if (!placeholder_count) { 7 | return; 8 | } 9 | // 2. If yes, then load the same page with full_render=1 query parameter again by Ajax 10 | let url = location.href.replace(/#.*/, ''); 11 | if (url.indexOf('?') === -1) { 12 | url += '?include_by_ajax_full_render=1'; 13 | } else { 14 | url += '&include_by_ajax_full_render=1'; 15 | } 16 | // 3. Load the page again by Ajax and with additional parameter 17 | $.ajax({ 18 | async: true, 19 | url: url, 20 | dataType: 'html' 21 | }).done(function(responseHTML, textStatus) { 22 | // 4. For each placeholder fill in the content 23 | let scriptsStack = []; 24 | $('
').append($.parseHTML(responseHTML, document, true)).find('.js-ajax-placeholder>*').each(function(index, element) { 25 | // collect scripts to a stack 26 | let $element = $(element); 27 | $element.find('script').each(function() { 28 | scriptsStack.push($(this)); 29 | // remove the script from the DOM, 30 | // so that it's not executed by jQuery with async: false 31 | $(this).remove(); 32 | }); 33 | 34 | $($placeholders[index]).replaceWith($element); 35 | }); 36 | 37 | // 5. Load and execute each script from the stack one by one 38 | function executeNextScriptFromStack() { 39 | let $script = scriptsStack.shift(); 40 | if ($script) { 41 | let src = $script.attr('src'); 42 | if (src) { 43 | $.ajax({ 44 | async: true, 45 | url: src, 46 | dataType: 'script' 47 | }).done(function(script, textStatus) { 48 | executeNextScriptFromStack(); 49 | }).fail(function(jqxhr, settings, exception) { 50 | executeNextScriptFromStack(); 51 | }); 52 | } else { 53 | $.globalEval($script.html()); 54 | executeNextScriptFromStack(); 55 | } 56 | } else { 57 | // 6. Trigger a special event "include_by_ajax_all_loaded" 58 | $(document).trigger('include_by_ajax_all_loaded'); 59 | } 60 | } 61 | executeNextScriptFromStack(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi](https://img.shields.io/pypi/v/django-include-by-ajax.svg)](https://pypi.python.org/pypi/django-include-by-ajax/) 2 | 3 | # A Django App Providing the `{% include_by_ajax %}` Template Tag 4 | 5 | ## The Problem 6 | 7 | Start pages usually show data aggregated from different sections. To render a start page might take some time if the relations and filters are complex or if there are a lot of images. The best practice for performance is to display the content above the fold (in the visible viewport area) as soon as possible, and then to load the rest of the page dynamically by JavaScript. 8 | 9 | ## The Solution 10 | 11 | This app allows you to organize heavy pages into sections which are included in the main page template. The default including can be done by the `{% include template_name %}` template tag and it is rendered immediately. We are introducing a new template tag `{% include_by_ajax template_name %}` which will initially render an empty placeholder, but then will load the content by Ajax dynamically. 12 | 13 | The template included by `{% include_by_ajax template_name %}` will get all the context that would normally be passed to a normal `{% include template_name %}` template tag. 14 | 15 | You can also pass a placeholder template which will be shown until the content is loaded. For this use `{% include_by_ajax template_name placeholder_template_name=placeholder_template_name %}` 16 | 17 | ## Implementation Details 18 | 19 | When you use the `{% include_by_ajax template_name %}`, the page is loaded and rendered twice: 20 | 21 | - At first, it is loaded and rendered minimally with empty placeholders `
`. 22 | - Then, some JavaScript loads it fully by Ajax and replaces placeholders with their content. 23 | 24 | The templates that you include by Ajax can contain `