Skip to content

Commit

Permalink
Fixes #300, failsafe request checking
Browse files Browse the repository at this point in the history
  • Loading branch information
karolyi authored and fjsj committed Oct 3, 2021
1 parent 524c0ca commit b19b39d
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 30 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,16 @@ The `is_preload=True` option in the `render_bundle` template tag can be used to
</html>
```
### Skipping the generation of multiple common chunks
You can use the parameter `skip_common_chunks=True` to specify that you don't want an already generated chunk be generated again in the same page.
In order for this option to work, django-webpack-loader requires the `request` object to be in the context, to be able to keep track of the generated chunks.
The `request` object is passed by default via the `django.template.context_processors.request` middleware with using the Django built-in templating system, and also with using Jinja2.
If you don't have `request` in the context for some reason (e.g. using `Template.render` or `render_to_string` directly without passing the request), you'll get warnings on the console.
### Appending file extensions
The `suffix` option can be used to append a string at the end of the file URL. For instance, it can be used if your webpack configuration emits compressed `.gz` files.
Expand Down
15 changes: 15 additions & 0 deletions tests/app/templates/home-duplicated.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Example</title>
{{ render_bundle('app1', 'js') }}
{{ render_bundle('app2', 'js') }}
</head>

<body>
<div id="react-app"></div>
{{ render_bundle('app1', 'js') }}
{{ render_bundle('app2', 'js') }}
</body>
</html>
147 changes: 145 additions & 2 deletions tests/app/tests/test_webpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@
from shutil import rmtree
from subprocess import call
from threading import Thread
from unittest.mock import Mock
from unittest.mock import call as MockCall
from unittest.mock import patch

from django.conf import settings
from django.template import engines
from django.template.backends.django import Template
from django.template import Context, Template, engines
from django.template.response import TemplateResponse
from django.test.client import RequestFactory
from django.test.testcases import TestCase
from django.views.generic.base import TemplateView
from django_jinja.backend import Jinja2
from django_jinja.backend import Template as Jinja2Template
from django_jinja.builtins import DEFAULT_EXTENSIONS

from webpack_loader.exceptions import (WebpackBundleLookupError, WebpackError,
WebpackLoaderBadStatsError,
WebpackLoaderTimeoutError)
from webpack_loader.templatetags.webpack_loader import _WARNING_MESSAGE
from webpack_loader.utils import get_loader

BUNDLE_PATH = os.path.join(
settings.BASE_DIR, 'assets/django_webpack_loader_bundles/')
DEFAULT_CONFIG = 'DEFAULT'
_OUR_EXTENSION = 'webpack_loader.contrib.jinja2ext.WebpackExtension'

_warn_mock = Mock()


class LoaderTestCase(TestCase):
def setUp(self):
Expand Down Expand Up @@ -333,6 +340,142 @@ def test_request_blocking(self):
elapsed = time.time() - then
self.assertTrue(elapsed < wait_for)

@patch(
target='webpack_loader.templatetags.webpack_loader.warn',
new=_warn_mock)
def test_emits_warning_on_no_request_in_djangoengine(self):
"""
Should emit warnings on having no request in context (django
template).
"""
self.compile_bundles('webpack.config.skipCommon.js')
asset_vendor = (
'<script src="/static/django_webpack_loader_bundles/vendors.js" >'
'</script>')
asset_app1 = (
'<script src="/static/django_webpack_loader_bundles/app1.js" >'
'</script>')
asset_app2 = (
'<script src="/static/django_webpack_loader_bundles/app2.js" >'
'</script>')

# Shouldn't call any `warn()` here
dups_template = Template(template_string=(
r'{% load render_bundle from webpack_loader %}'
r'{% render_bundle "app1" %}'
r'{% render_bundle "app2" %}')) # type: Template
output = dups_template.render(context=Context())
_warn_mock.assert_not_called()
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 2)

# Should call `warn()` here
nodups_template = Template(template_string=(
r'{% load render_bundle from webpack_loader %}'
r'{% render_bundle "app1" %}'
r'{% render_bundle "app2" skip_common_chunks=True %}')
) # type: Template
output = nodups_template.render(context=Context())
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 2)
_warn_mock.assert_called_once_with(
message=_WARNING_MESSAGE, category=RuntimeWarning)

# Should NOT call `warn()` here
_warn_mock.reset_mock()
nodups_template = Template(template_string=(
r'{% load render_bundle from webpack_loader %}'
r'{% render_bundle "app1" %}'
r'{% render_bundle "app2" skip_common_chunks=True %}')
) # type: Template
request = self.factory.get(path='/')
output = nodups_template.render(context=Context({'request': request}))
used_tags = getattr(request, '_webpack_loader_used_tags', None)
self.assertIsNotNone(used_tags, msg=(
'_webpack_loader_used_tags should be a property of request!'))
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 1)
_warn_mock.assert_not_called()
_warn_mock.reset_mock()

@patch(
target='webpack_loader.templatetags.webpack_loader.warn',
new=_warn_mock)
def test_emits_warning_on_no_request_in_jinja2engine(self):
'Should emit warnings on having no request in context (Jinja2).'
self.compile_bundles('webpack.config.skipCommon.js')
settings = {
'TEMPLATES': [
{
'NAME': 'jinja2',
'BACKEND': 'django_jinja.backend.Jinja2',
'APP_DIRS': True,
'OPTIONS': {
'match_extension': '.jinja',
'extensions': DEFAULT_EXTENSIONS + [_OUR_EXTENSION],
}
},
]
}
asset_vendor = (
'<script src="/static/django_webpack_loader_bundles/vendors.js" >'
'</script>')
asset_app1 = (
'<script src="/static/django_webpack_loader_bundles/app1.js" >'
'</script>')
asset_app2 = (
'<script src="/static/django_webpack_loader_bundles/app2.js" >'
'</script>')
warning_call = MockCall(
message=_WARNING_MESSAGE, category=RuntimeWarning)

# Shouldn't call any `warn()` here
with self.settings(**settings):
jinja2_engine = engines['jinja2'] # type: Jinja2
dups_template = jinja2_engine.get_template(
template_name='home-duplicated.jinja') # type: Jinja2Template
output = dups_template.render()
_warn_mock.assert_not_called()
self.assertEqual(output.count(asset_app1), 2)
self.assertEqual(output.count(asset_app2), 2)
self.assertEqual(output.count(asset_vendor), 4)

# Should call `warn()` here
with self.settings(**settings):
jinja2_engine = engines['jinja2'] # type: Jinja2
nodups_template = jinja2_engine.get_template(
template_name='home-deduplicated.jinja'
) # type: Jinja2Template
output = nodups_template.render()
self.assertEqual(output.count(asset_app1), 2)
self.assertEqual(output.count(asset_app2), 2)
self.assertEqual(output.count(asset_vendor), 4)
self.assertEqual(_warn_mock.call_count, 3)
self.assertListEqual(
_warn_mock.call_args_list,
[warning_call, warning_call, warning_call])

# Should NOT call `warn()` here
_warn_mock.reset_mock()
request = self.factory.get(path='/')
with self.settings(**settings):
jinja2_engine = engines['jinja2'] # type: Jinja2
nodups_template = jinja2_engine.get_template(
template_name='home-deduplicated.jinja'
) # type: Jinja2Template
output = nodups_template.render(request=request)
used_tags = getattr(request, '_webpack_loader_used_tags', None)
self.assertIsNotNone(used_tags, msg=(
'_webpack_loader_used_tags should be a property of request!'))
self.assertEqual(output.count(asset_app1), 1)
self.assertEqual(output.count(asset_app2), 1)
self.assertEqual(output.count(asset_vendor), 1)
_warn_mock.assert_not_called()
_warn_mock.reset_mock()

def test_skip_common_chunks_djangoengine(self):
"""Test case for deduplication of modules with the django engine."""
self.compile_bundles('webpack.config.skipCommon.js')
Expand Down
43 changes: 15 additions & 28 deletions webpack_loader/templatetags/webpack_loader.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
from django.conf import settings
from warnings import warn

from django.template import Library
from django.utils.safestring import mark_safe

from .. import utils

register = Library()
_PROC_DJTEMPLATE = 'django.template.context_processors.request'
_DJ_TEMPLATEPROCESSOR = 'django.template.backends.django.DjangoTemplates'
_STARTUP_ERROR = (
f'Please make sure that "{_PROC_DJTEMPLATE}" is added '
'to your ["OPTIONS"]["context_processors"] list in your '
f'settings.TEMPLATES where the BACKEND is "{_DJ_TEMPLATEPROCESSOR}". '
'django-webpack-loader needs it and cannot run without it.')


def _is_request_in_context():
for item in settings.TEMPLATES:
backend = item.get('BACKEND', {})
if backend == _DJ_TEMPLATEPROCESSOR:
processors = set(
item.get('OPTIONS', {}).get('context_processors', []))
if _PROC_DJTEMPLATE not in processors:
raise RuntimeError(_STARTUP_ERROR)


# Check settings at module import time
_is_request_in_context()
_WARNING_MESSAGE = (
'You have specified skip_common_chunks=True but the passed context '
'doesn\'t have a request. django_webpack_loader needs a request object to '
'filter out duplicate chunks. Please see https://github.com/django-webpack'
'/django-webpack-loader#skipping-the-generation-of-multiple-common-chunks')


@register.simple_tag(takes_context=True)
Expand All @@ -35,15 +20,17 @@ def render_bundle(
tags = utils.get_as_tags(
bundle_name, extension=extension, config=config, suffix=suffix,
attrs=attrs, is_preload=is_preload)

if not hasattr(context['request'], '_webpack_loader_used_tags'):
context['request']._webpack_loader_used_tags = set()

used_tags = context['request']._webpack_loader_used_tags
request = context.get('request')
if request is None:
if skip_common_chunks:
warn(message=_WARNING_MESSAGE, category=RuntimeWarning)
return mark_safe('\n'.join(tags))
used_tags = getattr(context['request'], '_webpack_loader_used_tags', None)
if not used_tags:
used_tags = context['request']._webpack_loader_used_tags = set()
if skip_common_chunks:
tags = [tag for tag in tags if tag not in used_tags]
used_tags.update(tags)

return mark_safe('\n'.join(tags))


Expand Down

0 comments on commit b19b39d

Please sign in to comment.