├── 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 | [](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 `