Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions blog/templatetags/blog_extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from html.parser import HTMLParser

from django import template
from django.utils.html import format_html

register = template.Library()


class LazyLoadingHTMLParser(HTMLParser):
def __init__(self):
super().__init__()
self.result = []

def handle_starttag(self, tag, attrs):
if tag.lower() == "img":
attrs_dict = dict(attrs)
attrs_dict.setdefault("loading", "lazy")
attrs_dict.setdefault("decoding", "async")
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs_dict.items())
self.result.append(f"<{tag} {attrs_str}>")
else:
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs)
self.result.append(f"<{tag}{' ' + attrs_str if attrs_str else ''}>")

def handle_endtag(self, tag):
self.result.append(f"</{tag}>")

def handle_data(self, data):
self.result.append(data)

def handle_startendtag(self, tag, attrs):
# For self-closing tags like <img />
if tag.lower() == "img":
attrs_dict = dict(attrs)
attrs_dict.setdefault("loading", "lazy")
attrs_dict.setdefault("decoding", "async")
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs_dict.items())
self.result.append(f"<{tag} {attrs_str} />")
else:
attrs_str = " ".join(f'{k}="{v}"' for k, v in attrs)
self.result.append(f"<{tag}{' ' + attrs_str if attrs_str else ''} />")


@register.filter
def add_lazy_loading(html):
parser = LazyLoadingHTMLParser()
parser.feed(html)
return format_html("".join(parser.result))
28 changes: 28 additions & 0 deletions blog/tests/test_lazy_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.test import SimpleTestCase

from blog.templatetags.blog_extras import add_lazy_loading


class AddLazyLoadingFilterTests(SimpleTestCase):
def test_adds_attributes_to_img_without_them(self):
html = '<p>Example <img src="example.jpg"></p>'
result = add_lazy_loading(html)
self.assertIn('loading="lazy"', result)
self.assertIn('decoding="async"', result)

def test_does_not_override_existing_attributes(self):
html = '<p><img src="test.jpg" loading="eager" decoding="sync"></p>'
result = add_lazy_loading(html)
self.assertIn('loading="eager"', result)
self.assertIn('decoding="sync"', result)

def test_handles_multiple_images(self):
html = '<img src="a.jpg"><img src="b.jpg">'
result = add_lazy_loading(html)
self.assertEqual(result.count('loading="lazy"'), 2)
self.assertEqual(result.count('decoding="async"'), 2)

def test_non_image_tags_are_untouched(self):
html = "<p>No images here</p>"
result = add_lazy_loading(html)
self.assertEqual(result, html)
17 changes: 17 additions & 0 deletions djangoproject/settings/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .common import INSTALLED_APPS

DEBUG = False
SECRET_KEY = "test-secret-key"

INSTALLED_APPS = [app for app in INSTALLED_APPS if app != "foundation"]

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}

PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
]
6 changes: 5 additions & 1 deletion djangoproject/templates/blog/entry_detail.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "base_weblog.html" %}
{% load i18n %}
{% load blog_extras %} {# Load your custom filter #}

{% block title %}{{ object.headline|escape }} | Weblog{% endblock %}

Expand All @@ -17,5 +18,8 @@ <h1>{{ object.headline|safe }}</h1>
Posted by <strong>{{ author }}</strong> on {{ pub_date }}
{% endblocktranslate %}
</span>
<div class="blog-entry-body">{{ object.body_html|safe }}</div>

<div class="blog-entry-body">
{{ object.body_html|add_lazy_loading|safe }}
</div>
{% endblock %}
5 changes: 3 additions & 2 deletions djangoproject/templates/foundation/meeting_detail.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "base_foundation.html" %}
{% load i18n %}
{% load blog_extras %} {# Load your custom filter #}

{% block og_title %}{% blocktranslate %}Meeting minutes: {{ meeting }}{% endblocktranslate %}{% endblock %}
{% block og_description %}{% blocktranslate %}Meeting minutes for {{ meeting }}{% endblocktranslate %}{% endblock %}
Expand Down Expand Up @@ -80,7 +81,7 @@ <h2>{% translate "Ongoing business" %}</h2>
{% for business in ongoing_business %}
<h3>{{ business.title }}</h3>

{{ business.body_html|safe }}
{{ business.body_html|add_lazy_loading|safe }}
{% endfor %}
{% endif %}

Expand All @@ -90,7 +91,7 @@ <h2>{% translate "New business" %}</h2>
{% for business in new_business %}
<h3>{{ business.title }}</h3>

{{ business.body_html|safe }}
{{ business.body_html|add_lazy_loading|safe }}
{% endfor %}
{% endif %}

Expand Down
23 changes: 23 additions & 0 deletions djangoproject/templates/foundation/tests/test_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.template import Context, Template
from django.test import TestCase

from foundation.models import Business, Meeting


class LazyLoadingMeetingTemplateTests(TestCase):
def test_meeting_body_html_has_lazy_loading(self):
meeting = Meeting.objects.create(title="Test Meeting")
business = Business.objects.create(
title="Test Business", body_html='<p><img src="img.jpg"></p>'
)
meeting.ongoing_business.add(business)

template = Template(
"""
{% load blog_extras %}
{{ business.body_html|add_lazy_loading|safe }}
"""
)
rendered = template.render(Context({"business": business}))
self.assertIn('loading="lazy"', rendered)
self.assertIn('decoding="async"', rendered)