├── .github
└── workflows
│ ├── django.yml
│ └── python-publish.yml
├── .gitignore
├── LICENSE
├── README.rst
├── pyproject.toml
├── src
└── django_webp
│ ├── __init__.py
│ ├── context_processors.py
│ ├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── clean_webp_images.py
│ ├── middleware.py
│ ├── templatetags
│ ├── __init__.py
│ └── webp.py
│ └── utils.py
└── tests
├── __init__.py
├── test_WEBPImageConverter.py
├── test_context_processor.py
├── test_main.py
├── test_templatetags.py
└── testproj
├── manage.py
├── static
└── django_webp
│ └── python.png
├── testapp
├── __init__.py
├── admin.py
├── apps.py
├── context_processors.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── clean_webp_images.py
├── middleware.py
├── migrations
│ └── __init__.py
├── models.py
├── templates
│ └── django_webp
│ │ └── index.html
├── templatetags
│ ├── __init__.py
│ └── webp.py
├── tests.py
├── urls.py
├── utils.py
└── views.py
└── testproj
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
/.github/workflows/django.yml:
--------------------------------------------------------------------------------
1 | name: Django CI
2 |
3 | on:
4 | push:
5 | branches: [ master, dev ]
6 | pull_request:
7 | branches: [ master, dev ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | max-parallel: 4
15 | matrix:
16 | python-version: ['3.8', '3.x']
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v2
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install SO Dependencies
25 | run: |
26 | sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev
27 | python -m pip install --upgrade pip
28 | - name: Install django dependencies
29 | run: pip install -e .
30 | - name: Install coveralls
31 | run: pip install coveralls
32 | - name: Run Tests
33 | run: |
34 | coverage run --source=django_webp manage.py test
35 | coverage lcov
36 | working-directory: tests
37 | - name: Coveralls GitHub Action
38 | uses: coverallsapp/github-action@v2
39 | with:
40 | github-token: ${{ secrets.GITHUB_TOKEN }}
41 | file: ./tests/coverage.lcov
42 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | old/
5 | .idea/
6 | .DS_Store
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | venv/
15 | bin/
16 | build/
17 | develop-eggs/
18 | dist/
19 | eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 |
29 | # Installer logs
30 | pip-log.txt
31 | pip-delete-this-directory.txt
32 |
33 | # Unit test / coverage reports
34 | htmlcov/
35 | .tox/
36 | .coverage
37 | .cache
38 | nosetests.xml
39 | coverage.xml
40 | coverage.lcov
41 |
42 | # Translations
43 | *.mo
44 |
45 | # Mr Developer
46 | .mr.developer.cfg
47 | .project
48 | .pydevproject
49 |
50 | # Rope
51 | .ropeproject
52 |
53 | # Django stuff:
54 | *.log
55 | *.pot
56 |
57 | # Sphinx documentation
58 | docs/_build/
59 |
60 | # Local
61 | WEBP_CACHE/
62 | media/
63 | staticfiles/
64 | db.sqlite3
65 | pypi.key
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 André Farzat
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.
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | django-webp
2 | ===========
3 |
4 | Speeds up static file load times by generating a webp image to load to a webpage instead of a jpg, gif or png
5 |
6 | |Build Status| |Coverage Status|
7 |
8 |
9 | Usage
10 | -----
11 |
12 | Load the ``webp`` module in your template and use the ``webp``
13 | templatetag to point to the image you want to convert.
14 |
15 | .. code:: html
16 |
17 | {% load webp %}
18 |
19 | {# Use webp as you would use static templatetag #}
20 |
21 |
28 |
29 | Installation
30 | ------------
31 |
32 | First, if you are using a version of Pillow <= 9.3.0, you must install webp support since earlier versions of Pillow do not
33 | have webp support built-in. In ubuntu, you can install via apt-get:
34 |
35 | .. code:: sh
36 |
37 | apt-get install libwebp-dev
38 |
39 | Please, check `the official guide`_ for the other systems.
40 |
41 | Then, install ``django-webp``.
42 |
43 | .. code:: sh
44 |
45 | pip install django-webp
46 |
47 | add it to ``INSTALLED_APPS`` configuration
48 |
49 | .. code:: python
50 |
51 | INSTALLED_APPS = (
52 | 'django.contrib.staticfiles',
53 | 'django_webp',
54 | '...',
55 | )
56 |
57 | edit your installation of Whitenoise to use our slightly modded version of it
58 |
59 | .. code:: python
60 |
61 | MIDDLEWARE = [
62 | 'django.middleware.security.SecurityMiddleware',
63 | 'django_webp.middleware.ModdedWhiteNoiseMiddleware',
64 | '...',
65 | ]
66 |
67 | add the django\_webp context processor
68 |
69 | .. code:: python
70 |
71 | TEMPLATES = [
72 | {
73 | '...'
74 | 'OPTIONS': {
75 | 'context_processors': [
76 | '...',
77 | 'django_webp.context_processors.webp',
78 | ],
79 | },
80 | },
81 | ]
82 |
83 | Settings
84 | --------
85 |
86 | The following Django-level settings affect the behavior of the library
87 |
88 | - ``WEBP_CHECK_URLS``
89 |
90 | When set to ``True``, urls that link to externally stored images (i.e. images hosted by another site) are checked to confirm if they are valid image links.
91 | Ideally, this should temporarily be set to ``True`` whenever the ``WEBP_CACHE`` has been cleaned or if there has been substantial changes to your project's template files.
92 | This defaults to ``False``.
93 |
94 |
95 |
96 |
97 |
98 | Possible Issues
99 | -----------------
100 |
101 | - ``django-webp`` uses ``Pillow`` to convert the images. If you’ve installed the ``libwebp-dev`` after already installed ``Pillow``, it’s necessary to uninstall and install it back because it needs to be compiled with it.
102 | - This package was built specifically for production environments that use Whitenoise for serving static files so there currently isn't support for serving files via dedicated servers or through cloud services
103 |
104 | Cleaning the cache
105 | ------------------
106 |
107 | You can clean the cache running:
108 |
109 | .. code:: sh
110 |
111 | python manage.py clean_webp_images
112 |
113 | .. _the official guide: https://developers.google.com/speed/webp/docs/precompiled
114 |
115 | .. |Build Status| image:: https://github.com/andrefarzat/django-webp/actions/workflows/django.yml/badge.svg?branch=master
116 | :target: https://github.com/andrefarzat/django-webp/actions/workflows/django.yml
117 | .. |Coverage Status| image:: https://coveralls.io/repos/github/andrefarzat/django-webp/badge.svg?branch=master
118 | :target: https://coveralls.io/github/andrefarzat/django-webp?branch=master
119 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0.0", "wheel", "setuptools_scm[toml]>=6.2"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.setuptools_scm]
6 |
7 | [project]
8 | name = "django-webp"
9 | version = "3.0.0"
10 | description = "Serves a webp version of static images to browsers instead of jpg, gif or png"
11 | readme = "README.rst"
12 | requires-python = ">=3.5"
13 | keywords = ["django", "webp", "python"]
14 | authors = [
15 | { name = "Andre Farzat", email = "andrefarzat@gmail.com" },
16 | { name = "Daniel Opara", email = "daniel.opara@tufts.edu" }
17 | ]
18 | maintainers = [
19 | { name = "Andre Farzat", email = "andrefarzat@gmail.com" },
20 | { name = "Daniel Opara", email = "daniel.opara@tufts.edu" }
21 | ]
22 | license = { file = "LICENSE" }
23 | classifiers = [
24 | "Development Status :: 5 - Production/Stable",
25 | "Intended Audience :: Developers",
26 |
27 | "Topic :: Software Development :: Libraries :: Python Modules",
28 |
29 | "License :: OSI Approved :: MIT License",
30 | "Programming Language :: Python :: 3",
31 | "Programming Language :: Python :: 3 :: Only",
32 | ]
33 |
34 | dependencies = [
35 | "asgiref>=3.5.0",
36 | "django>=4.0.3",
37 | "pillow>=9.0.1",
38 | "sqlparse>=0.4.2",
39 | "whitenoise>=6.5.0"
40 | ]
41 |
42 | [project.optional-dependencies]
43 | dev = ["pip-tools", "pytest", "black"]
44 |
45 | [project.urls]
46 | Homepage = "http://pypi.python.org/pypi/django-webp/"
47 |
48 | [tool.setuptools]
49 | include-package-data = true
50 |
51 | [tool.setuptools.packages.find]
52 | namespaces = true
53 | where = ["src"]
54 |
55 |
--------------------------------------------------------------------------------
/src/django_webp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/src/django_webp/__init__.py
--------------------------------------------------------------------------------
/src/django_webp/context_processors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | def _check_by_http_accept_header(http_accept):
5 | return "webp" in http_accept
6 |
7 |
8 | def webp(request):
9 | """Adds `supports_webp` value in the context"""
10 | http_accept = request.META.get("HTTP_ACCEPT", "")
11 |
12 | if _check_by_http_accept_header(http_accept):
13 | supports_webp = True
14 | else:
15 | supports_webp = False
16 |
17 | return {"supports_webp": supports_webp}
18 |
--------------------------------------------------------------------------------
/src/django_webp/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/src/django_webp/management/__init__.py
--------------------------------------------------------------------------------
/src/django_webp/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/src/django_webp/management/commands/__init__.py
--------------------------------------------------------------------------------
/src/django_webp/management/commands/clean_webp_images.py:
--------------------------------------------------------------------------------
1 | import shutil
2 |
3 | from django.core.management.base import BaseCommand, CommandError
4 |
5 | from django_webp.utils import WEBP_STATIC_ROOT
6 |
7 |
8 | class Command(BaseCommand):
9 | help = "Removes all cached webp images"
10 |
11 | def handle(self, *args, **options):
12 | try:
13 | shutil.rmtree(WEBP_STATIC_ROOT)
14 | self.stdout.write("Folder %s removed" % WEBP_STATIC_ROOT) # pragma: no cover
15 | except:
16 | raise CommandError("Folder %s was already removed" % WEBP_STATIC_ROOT)
17 |
--------------------------------------------------------------------------------
/src/django_webp/middleware.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | from posixpath import basename
5 | from urllib.parse import urlparse
6 |
7 | from django.conf import settings
8 | from django.contrib.staticfiles import finders
9 | from django.contrib.staticfiles.storage import staticfiles_storage
10 | from django.http import FileResponse
11 | from django.urls import get_script_prefix
12 |
13 | from whitenoise.base import WhiteNoise
14 | from whitenoise.string_utils import ensure_leading_trailing_slash
15 |
16 | __all__ = ["WhiteNoiseMiddleware"]
17 |
18 | class WhiteNoiseFileResponse(FileResponse):
19 | """
20 | Wrap Django's FileResponse to prevent setting any default headers. For the
21 | most part these just duplicate work already done by WhiteNoise but in some
22 | cases (e.g. the content-disposition header introduced in Django 3.0) they
23 | are actively harmful.
24 | """
25 |
26 | def set_headers(self, *args, **kwargs):
27 | pass
28 |
29 |
30 | class ModdedWhiteNoiseMiddleware(WhiteNoise):
31 | """
32 | Wrap WhiteNoise to allow it to function as Django middleware, rather
33 | than WSGI middleware.
34 | """
35 |
36 | def __init__(self, get_response=None, settings=settings):
37 | self.get_response = get_response
38 |
39 | try:
40 | autorefresh: bool = settings.WHITENOISE_AUTOREFRESH
41 | except AttributeError:
42 | autorefresh = settings.DEBUG
43 | try:
44 | max_age = settings.WHITENOISE_MAX_AGE
45 | except AttributeError:
46 | if settings.DEBUG:
47 | max_age = 0
48 | else:
49 | max_age = 60
50 | try:
51 | allow_all_origins = settings.WHITENOISE_ALLOW_ALL_ORIGINS
52 | except AttributeError:
53 | allow_all_origins = True
54 | try:
55 | charset = settings.WHITENOISE_CHARSET
56 | except AttributeError:
57 | charset = "utf-8"
58 | try:
59 | mimetypes = settings.WHITENOISE_MIMETYPES
60 | except AttributeError:
61 | mimetypes = None
62 | try:
63 | add_headers_function = settings.WHITENOISE_ADD_HEADERS_FUNCTION
64 | except AttributeError:
65 | add_headers_function = None
66 | try:
67 | index_file = settings.WHITENOISE_INDEX_FILE
68 | except AttributeError:
69 | index_file = None
70 | try:
71 | immutable_file_test = settings.WHITENOISE_IMMUTABLE_FILE_TEST
72 | except AttributeError:
73 | immutable_file_test = None
74 |
75 | super().__init__(
76 | application=None,
77 | autorefresh=autorefresh,
78 | max_age=max_age,
79 | allow_all_origins=allow_all_origins,
80 | charset=charset,
81 | mimetypes=mimetypes,
82 | add_headers_function=add_headers_function,
83 | index_file=index_file,
84 | immutable_file_test=immutable_file_test,
85 | )
86 |
87 | try:
88 | self.use_finders = settings.WHITENOISE_USE_FINDERS
89 | except AttributeError:
90 | self.use_finders = settings.DEBUG
91 |
92 | try:
93 | self.static_prefix = settings.WHITENOISE_STATIC_PREFIX
94 | except AttributeError:
95 | self.static_prefix = urlparse(settings.STATIC_URL or "").path
96 | script_prefix = get_script_prefix().rstrip("/")
97 | if script_prefix:
98 | if self.static_prefix.startswith(script_prefix):
99 | self.static_prefix = self.static_prefix[len(script_prefix) :]
100 | self.static_prefix = ensure_leading_trailing_slash(self.static_prefix)
101 |
102 | self.static_root = settings.STATIC_ROOT
103 | if self.static_root:
104 | self.add_files(self.static_root, prefix=self.static_prefix)
105 |
106 | try:
107 | root = settings.WHITENOISE_ROOT
108 | except AttributeError:
109 | root = None
110 | if root:
111 | self.add_files(root)
112 |
113 | if self.use_finders and not self.autorefresh:
114 | self.add_files_from_finders()
115 |
116 | def __call__(self, request):
117 | if self.autorefresh:
118 | static_file = self.find_file(request.path_info)
119 | else:
120 | static_file = self.files.get(request.path_info)
121 | if static_file is not None:
122 | return self.serve(static_file, request)
123 | if static_file is None:
124 | if self.static_prefix in request.path_info:
125 | # using path info and static root to check the directory of the recently made webp
126 | raw_full_path = os.path.join(self.static_root, (request.path_info).lstrip("/"))
127 | clean_full_path = raw_full_path.replace("\\", "/")
128 | complete_full_path = clean_full_path.replace(self.static_prefix.rstrip("/"), "", 1)
129 | if os.path.exists(complete_full_path):
130 | self.add_files(self.static_root, prefix=self.static_prefix)
131 | static_file = self.files.get(complete_full_path)
132 | if static_file is None:
133 | request.not_serving = request.path_info
134 | return self.serve(static_file, request)
135 | return self.get_response(request)
136 |
137 | @staticmethod
138 | def serve(static_file, request):
139 | try:
140 | response = static_file.get_response(request.method, request.META)
141 | status = int(response.status)
142 | http_response = WhiteNoiseFileResponse(response.file or (), status=status)
143 | # Remove default content-type
144 | del http_response["content-type"]
145 | for key, value in response.headers:
146 | http_response[key] = value
147 | return http_response
148 | except:
149 | http_response = WhiteNoiseFileResponse((), status=500)
150 | http_response['not serving'] = request.not_serving
151 | return http_response
152 |
153 |
154 | def add_files_from_finders(self):
155 | files = {}
156 | for finder in finders.get_finders():
157 | for path, storage in finder.list(None):
158 | prefix = (getattr(storage, "prefix", None) or "").strip("/")
159 | url = "".join(
160 | (
161 | self.static_prefix,
162 | prefix,
163 | "/" if prefix else "",
164 | path.replace("\\", "/"),
165 | )
166 | )
167 | # Use setdefault as only first matching file should be used
168 | files.setdefault(url, storage.path(path))
169 | stat_cache = {path: os.stat(path) for path in files.values()}
170 | for url, path in files.items():
171 | self.add_file_to_dictionary(url, path, stat_cache=stat_cache)
172 |
173 | def candidate_paths_for_url(self, url):
174 | if self.use_finders and url.startswith(self.static_prefix):
175 | path = finders.find(url[len(self.static_prefix) :])
176 | if path:
177 | yield path
178 | paths = super().candidate_paths_for_url(url)
179 | for path in paths:
180 | yield path
181 |
182 | def immutable_file_test(self, path, url):
183 | """
184 | Determine whether given URL represents an immutable file (i.e. a
185 | file with a hash of its contents as part of its name) which can
186 | therefore be cached forever
187 | """
188 | if not url.startswith(self.static_prefix):
189 | return False
190 | name = url[len(self.static_prefix) :]
191 | name_without_hash = self.get_name_without_hash(name)
192 | if name == name_without_hash:
193 | return False
194 | static_url = self.get_static_url(name_without_hash)
195 | # If the static_url function maps the name without hash
196 | # back to the original name, then we know we've got a
197 | # versioned filename
198 | if static_url and basename(static_url) == basename(url):
199 | return True
200 | return False
201 |
202 | def get_name_without_hash(self, filename):
203 | """
204 | Removes the version hash from a filename e.g, transforms
205 | 'css/application.f3ea4bcc2.css' into 'css/application.css'
206 |
207 | Note: this is specific to the naming scheme used by Django's
208 | CachedStaticFilesStorage. You may have to override this if
209 | you are using a different static files versioning system
210 | """
211 | name_with_hash, ext = os.path.splitext(filename)
212 | name = os.path.splitext(name_with_hash)[0]
213 | return name + ext
214 |
215 | def get_static_url(self, name):
216 | try:
217 | return staticfiles_storage.url(name)
218 | except ValueError:
219 | return None
220 |
--------------------------------------------------------------------------------
/src/django_webp/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/src/django_webp/templatetags/__init__.py
--------------------------------------------------------------------------------
/src/django_webp/templatetags/webp.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import logging
4 | import requests
5 | from io import BytesIO
6 | from PIL import Image
7 |
8 | from django import template
9 | from django.conf import settings
10 | from django.core.files.base import ContentFile
11 | from django.contrib.staticfiles import finders
12 | from django.core.files.storage import default_storage
13 | from django.templatetags.static import static
14 | from django.core.exceptions import SuspiciousFileOperation
15 |
16 | from whitenoise.middleware import WhiteNoiseMiddleware
17 | from whitenoise.string_utils import ensure_leading_trailing_slash
18 |
19 | from django.conf import settings
20 | from utils import (
21 | WEBP_STATIC_ROOT,
22 | WEBP_DEBUG,
23 | WEBP_CHECK_URLS,
24 | USING_WHITENOISE
25 | )
26 |
27 | # if STATIC_ROOT is abs, then we are likely woring in production, if not, likely a testing env
28 | if os.path.isabs(settings.STATIC_ROOT):
29 | base_path = settings.STATIC_ROOT
30 | static_dir_prefix = os.path.relpath(settings.STATIC_ROOT, start=settings.BASE_DIR)
31 | else:
32 | base_path = os.path.join(settings.BASE_DIR, settings.STATIC_ROOT)
33 | static_dir_prefix = settings.STATIC_ROOT
34 |
35 | # getting rid of trailing slash
36 | static_dir_prefix = (static_dir_prefix).rstrip("/")
37 |
38 | register = template.Library()
39 |
40 |
41 | class WEBPImageConverter:
42 | def __init__(self):
43 | self.logger = logging.getLogger(__name__)
44 |
45 | def generate_path(self, image_path):
46 | """creates all folders necessary until reach the file's folder"""
47 | folder_path = os.path.dirname(image_path)
48 | if not os.path.isdir(folder_path):
49 | os.makedirs(folder_path)
50 |
51 | def get_static_image(self, image_url):
52 | if "https://" in image_url:
53 | return image_url
54 | else:
55 | return static(image_url)
56 |
57 | def check_image_dirs(self, generated_path, image_path):
58 | """
59 | Checks if original image directory is valid and prevents duplicates of
60 | generated images by checking if the already exist in a directory
61 | """
62 |
63 | # Checking if original image exists
64 | if not os.path.exists(os.path.join(base_path, image_path)) and not "https" in image_path:
65 | self.logger.warn(f"Original image does not exist in static files path: {os.path.join(base_path, image_path)}")
66 | return False
67 |
68 | # Checks if the webp version of the image exists
69 | if os.path.exists(generated_path):
70 | return False
71 | else:
72 | return True
73 |
74 | def is_image_served(self, image_url, timeout_seconds=1):
75 | try:
76 | response = requests.head(image_url, timeout=timeout_seconds)
77 | if response.status_code == requests.codes.ok:
78 | return True
79 | else:
80 | return False
81 | except requests.exceptions.Timeout:
82 | return False
83 | except requests.exceptions.RequestException as e:
84 | return False
85 |
86 | def get_generated_image(self, image_url):
87 | """Returns the url to the webp gerenated image, returns the
88 | original image url if any of the following occur:
89 |
90 | - webp image generation fails
91 | - original image does not exist
92 | """
93 |
94 | if "https://" in image_url: # pragma: no cover
95 | # Split the text by forward slashes and gets the last part (characters after the last slash)
96 | raw_filename = image_url.split("/")[-1]
97 | real_url = os.path.join(
98 | "online_images/", os.path.splitext(raw_filename)[0] + ".webp"
99 | )
100 | else:
101 | real_url = os.path.splitext(image_url)[0] + ".webp"
102 |
103 | generated_path = os.path.join(WEBP_STATIC_ROOT, real_url).lstrip("/")
104 |
105 | # Checks if link provided is still valid
106 | # Only bothers to check if the link is valid if WEBP_CHECK_URLS is True
107 | if "https://" in image_url: # pragma: no cover
108 | if WEBP_CHECK_URLS:
109 | try:
110 | response = requests.head(image_url)
111 | if response.status_code == requests.codes.ok:
112 | content_type = response.headers.get("Content-Type", "")
113 | if not content_type.startswith("image/"):
114 | self.logger.warn(f"The following image url is invalid: {image_url}")
115 | except requests.RequestException:
116 | return self.get_static_image(image_url)
117 |
118 | should_generate = self.check_image_dirs(generated_path, image_url)
119 |
120 | if should_generate is True:
121 | if "https://" in image_url: # pragma: no cover
122 | if not self.generate_webp_image(generated_path, image_url):
123 | self.logger.error(f"Failed to generate from URL: {image_url}")
124 | return self.get_static_image(image_url)
125 | else:
126 | # Constructing full image path for original image
127 | image_path = os.path.join(base_path, image_url)
128 | if not self.generate_webp_image(generated_path, image_path):
129 | return self.get_static_image(image_url)
130 |
131 | ## converting generated_path from an absolute path to a relative path
132 | index = generated_path.find(static_dir_prefix)
133 | # Extract the substring starting from static_dir_prefix and replacing any weird backslashes with forward slashes
134 | generated_path = (generated_path[index + len(static_dir_prefix):]).replace("\\", "/")
135 |
136 |
137 | # have to check if image is served bc webp runs before whitenoise can properly hash the image dirs
138 | domain = os.environ.get("HOST", default="http://127.0.0.1:8000/")
139 | if self.is_image_served(domain.rstrip("/") + static(generated_path)):
140 | return static(generated_path)
141 | else:
142 | return self.get_static_image(image_url)
143 |
144 | def generate_webp_image(self, generated_path, image_path):
145 | final_path = os.path.join(str(base_path), generated_path)
146 |
147 | ## Generating images if they do not exist in a directory
148 | # Fetching the image data
149 | if "https://" in image_path: # pragma: no cover
150 | response = requests.get(image_path)
151 | try:
152 | image = Image.open(BytesIO(response.content))
153 | except:
154 | self.logger.error(f"Error: Failed to read the image file from URL: {image_path}")
155 | return False
156 | else:
157 | try:
158 | image = Image.open(image_path)
159 | except FileNotFoundError:
160 | return False
161 |
162 | # Using the image data to save a webp version of it to static files
163 | try:
164 | self.generate_path(generated_path)
165 | # we use a buffer to store the contents of our conversion before saving to disk
166 | buffer = BytesIO()
167 | image.save(buffer, "WEBP")
168 | content_file = ContentFile(buffer.getvalue())
169 | default_storage.save(final_path, content_file)
170 | image.close()
171 | return True
172 | except KeyError: # pragma: no cover
173 | self.logger.error("WEBP is not installed in Pillow")
174 | except (IOError, OSError): # pragma: no cover
175 | self.logger.error("WEBP image could not be saved in %s" % generated_path)
176 | except SuspiciousFileOperation:
177 | self.logger.error("SuspiciousFileOperation: the generated image was created outside of the base project path %s" % generated_path)
178 |
179 | return False # pragma: no cover
180 |
181 |
182 | @register.simple_tag(takes_context=True)
183 | def webp(context, value, force_static=WEBP_DEBUG):
184 | converter = WEBPImageConverter()
185 |
186 | supports_webp = context.get("supports_webp", False)
187 | if not supports_webp or not force_static:
188 | return converter.get_static_image(value)
189 |
190 | return converter.get_generated_image(value)
191 |
--------------------------------------------------------------------------------
/src/django_webp/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.conf import settings
4 | from django.core.exceptions import ImproperlyConfigured
5 | from django.templatetags.static import static
6 |
7 | BASE_DIR = getattr(settings, "BASE_DIR", "")
8 |
9 | WEBP_CHECK_URLS = getattr(settings, "WEBP_CHECK_URLS", "") or False
10 |
11 | WEBP_STATIC_URL = static(getattr(settings, "WEBP_STATIC_URL", "WEBP_CACHE/"))
12 | if os.path.isabs(settings.STATIC_ROOT):
13 | WEBP_STATIC_ROOT = os.path.join(settings.STATIC_ROOT, "WEBP_CACHE/")
14 | else:
15 | WEBP_STATIC_ROOT = os.path.join(settings.BASE_DIR, settings.STATIC_ROOT, "WEBP_CACHE/")
16 |
17 | WEBP_DEBUG = getattr(settings, "WEBP_DEBUG", settings.DEBUG)
18 |
19 | if not WEBP_STATIC_URL.endswith("/"): # pragma: no cover
20 | raise ImproperlyConfigured("If set, WEBP_STATIC_URL must end with a slash")
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_WEBPImageConverter.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.test import TestCase
4 | from django.conf import settings
5 |
6 | from django_webp.templatetags.webp import WEBPImageConverter
7 |
8 |
9 | class WEBPImageConverterTestCase(TestCase):
10 |
11 | def test_generate_webp_image_when_file_doesnt_exist(self):
12 | image_path = os.path.join(settings.BASE_DIR, 'testapp', 'static', 'django_webp', 'does-not-exist.png')
13 | generated_path = os.path.join(settings.BASE_DIR, 'staticfiles', 'python.webp')
14 |
15 | converter = WEBPImageConverter()
16 | self.assertFalse(converter.generate_webp_image(generated_path, image_path))
17 |
18 | def test_get_static_image(self):
19 | converter = WEBPImageConverter()
20 | self.assertEqual(converter.get_static_image("https://example.com/image.png"), "https://example.com/image.png")
21 | self.assertEqual(converter.get_static_image("image.png"), "/static/image.png")
--------------------------------------------------------------------------------
/tests/test_context_processor.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from django.http import HttpRequest
4 |
5 | from django_webp.context_processors import webp
6 |
7 |
8 | class ContextProcessorTest(unittest.TestCase):
9 | def setUp(self):
10 | self.request = HttpRequest()
11 |
12 | def test_common_return(self):
13 | """Must return False in the dic"""
14 | result = webp(self.request)
15 | supports_webp = result.get("supports_webp", None)
16 | self.assertFalse(supports_webp)
17 |
18 |
19 | def test_by_http_accept_header(self):
20 | """
21 | Giving a valid http accept header, shold return True
22 | @see -> https://www.igvita.com/2013/05/01/deploying-webp-via-accept-content-negotiation/
23 | """
24 | self.request.META["HTTP_ACCEPT"] = "image/webp"
25 | result = webp(self.request)
26 | supports_webp = result.get("supports_webp", None)
27 | self.assertTrue(supports_webp)
28 |
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import unittest
4 | from PIL import Image
5 |
6 | from django.test.client import Client
7 | from django.contrib.staticfiles import finders
8 | from django.core.management import call_command
9 | from django.core.management.base import CommandError
10 |
11 |
12 | IMAGE_PNG_PATH = finders.find('django_webp/python.png')
13 |
14 |
15 | class MainTest(unittest.TestCase):
16 |
17 | def test_pillow(self):
18 | """ Checks if current pillow installation
19 | has support to WEBP """
20 | image = Image.open(IMAGE_PNG_PATH)
21 | try:
22 | image.load()
23 | self.assertTrue(True)
24 | except:
25 | self.assertFail("There is no support for webp")
26 |
27 |
28 | def test_index(self):
29 | client = Client()
30 | response = client.get('/')
31 |
32 | self.assertEqual(response.status_code, 200)
33 |
34 |
35 | def test_clean_webp_images_command(self):
36 | call_command('collectstatic', '--noinput', '--clear')
37 | self.assertRaises(CommandError, call_command, 'clean_webp_images')
38 |
--------------------------------------------------------------------------------
/tests/test_templatetags.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import os
3 | import shutil
4 |
5 | from django.conf import settings
6 | from django.core.files.images import ImageFile
7 | from django.core.files.storage import FileSystemStorage
8 | from django.template import Template, Context
9 | from django.test.utils import override_settings
10 | from django.templatetags.static import static
11 |
12 | from django_webp.templatetags.webp import webp
13 | from utils import WEBP_STATIC_URL, WEBP_STATIC_ROOT
14 |
15 |
16 | class TemplateTagTest(unittest.TestCase):
17 |
18 | def setUp(self):
19 | self.supported_url = WEBP_STATIC_URL + 'django_webp/python.webp'
20 | self.unsupported_url = static('django_webp/python.png')
21 |
22 | def tearDown(self):
23 | # cleaning the folder the files here
24 | try:
25 | shutil.rmtree(WEBP_STATIC_ROOT)
26 | except:
27 | pass
28 |
29 | def _assertFile(self, file_path, msg=''):
30 | STATIC_ROOT = settings.STATIC_ROOT if settings.STATIC_ROOT.endswith('/') else settings.STATIC_ROOT + '/'
31 | staticfile_path = file_path.replace(settings.STATIC_URL, STATIC_ROOT)
32 | file_exist = os.path.isfile(staticfile_path)
33 |
34 | msg = msg or ('file doesnt exist: %s' % staticfile_path)
35 | self.assertTrue(file_exist, msg)
36 |
37 | def _get_valid_context(self):
38 | return Context({'supports_webp': True})
39 |
40 |
41 | def _get_invalid_context(self):
42 | return Context({'supports_webp': False})
43 |
44 |
45 | def _render_template(self, html, context={}):
46 | return Template(html).render(Context(context))
47 |
48 |
49 | def test_unexistent_image(self):
50 | """ If the given image doesn't exist, return the static url """
51 | context = self._get_valid_context()
52 | result = webp(context, 'django_webp/this_image_doesnt_exist.gif')
53 | expected = static('django_webp/this_image_doesnt_exist.gif')
54 | self.assertEqual(result, expected)
55 |
56 |
57 | def test_supports_webp_false(self):
58 | context = self._get_invalid_context()
59 | result = webp(context, 'django_webp/python.png')
60 | expected = static('django_webp/python.png')
61 | self.assertEqual(result, expected)
62 |
63 |
64 | def test_templatetag(self):
65 | """ checks the returned url from the webp function """
66 | context = self._get_valid_context()
67 | result = webp(context, 'django_webp/python.png')
68 | self.assertEqual(self.supported_url, result)
69 | self._assertFile(result, 'file %s should have been created' % result)
70 |
71 |
72 | def test_templatetag_in_template(self):
73 | html = '{% load webp %}{% webp "django_webp/python.png" %}'
74 | rendered = self._render_template(html, context=self._get_valid_context())
75 | self.assertEqual(self.supported_url, rendered)
76 | self._assertFile(rendered, 'file %s should have been created' % rendered)
77 |
78 |
79 | def test_debug_true(self):
80 | """ if DEBUG = True, should always return the static url """
81 | context = self._get_valid_context()
82 | result = webp(context, 'django_webp/python.png', force_static=True)
83 | self.assertEqual(self.unsupported_url, result)
84 |
--------------------------------------------------------------------------------
/tests/testproj/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproj.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/tests/testproj/static/django_webp/python.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/tests/testproj/static/django_webp/python.png
--------------------------------------------------------------------------------
/tests/testproj/testapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/tests/testproj/testapp/__init__.py
--------------------------------------------------------------------------------
/tests/testproj/testapp/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/tests/testproj/testapp/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TestappConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'testapp'
7 |
--------------------------------------------------------------------------------
/tests/testproj/testapp/context_processors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | def _check_by_http_accept_header(http_accept):
5 | return "webp" in http_accept
6 |
7 |
8 | def webp(request):
9 | """Adds `supports_webp` value in the context"""
10 | http_accept = request.META.get("HTTP_ACCEPT", "")
11 |
12 | if _check_by_http_accept_header(http_accept):
13 | supports_webp = True
14 | else:
15 | supports_webp = False
16 |
17 | return {"supports_webp": supports_webp}
18 |
--------------------------------------------------------------------------------
/tests/testproj/testapp/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/tests/testproj/testapp/management/__init__.py
--------------------------------------------------------------------------------
/tests/testproj/testapp/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/tests/testproj/testapp/management/commands/__init__.py
--------------------------------------------------------------------------------
/tests/testproj/testapp/management/commands/clean_webp_images.py:
--------------------------------------------------------------------------------
1 | import shutil
2 |
3 | from django.core.management.base import BaseCommand, CommandError
4 |
5 | from django_webp.utils import WEBP_STATIC_ROOT
6 |
7 |
8 | class Command(BaseCommand):
9 | help = "Removes all cached webp images"
10 |
11 | def handle(self, *args, **options):
12 | try:
13 | shutil.rmtree(WEBP_STATIC_ROOT)
14 | self.stdout.write("Folder %s removed" % WEBP_STATIC_ROOT) # pragma: no cover
15 | except:
16 | raise CommandError("Folder %s was already removed" % WEBP_STATIC_ROOT)
17 |
--------------------------------------------------------------------------------
/tests/testproj/testapp/middleware.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | from posixpath import basename
5 | from urllib.parse import urlparse
6 |
7 | from django.conf import settings
8 | from django.contrib.staticfiles import finders
9 | from django.contrib.staticfiles.storage import staticfiles_storage
10 | from django.http import FileResponse
11 | from django.urls import get_script_prefix
12 |
13 | from whitenoise.base import WhiteNoise
14 | from whitenoise.string_utils import ensure_leading_trailing_slash
15 |
16 | __all__ = ["WhiteNoiseMiddleware"]
17 |
18 | class WhiteNoiseFileResponse(FileResponse):
19 | """
20 | Wrap Django's FileResponse to prevent setting any default headers. For the
21 | most part these just duplicate work already done by WhiteNoise but in some
22 | cases (e.g. the content-disposition header introduced in Django 3.0) they
23 | are actively harmful.
24 | """
25 |
26 | def set_headers(self, *args, **kwargs):
27 | pass
28 |
29 |
30 | class ModdedWhiteNoiseMiddleware(WhiteNoise):
31 | """
32 | Wrap WhiteNoise to allow it to function as Django middleware, rather
33 | than WSGI middleware.
34 | """
35 |
36 | def __init__(self, get_response=None, settings=settings):
37 | self.get_response = get_response
38 |
39 | try:
40 | autorefresh: bool = settings.WHITENOISE_AUTOREFRESH
41 | except AttributeError:
42 | autorefresh = settings.DEBUG
43 | try:
44 | max_age = settings.WHITENOISE_MAX_AGE
45 | except AttributeError:
46 | if settings.DEBUG:
47 | max_age = 0
48 | else:
49 | max_age = 60
50 | try:
51 | allow_all_origins = settings.WHITENOISE_ALLOW_ALL_ORIGINS
52 | except AttributeError:
53 | allow_all_origins = True
54 | try:
55 | charset = settings.WHITENOISE_CHARSET
56 | except AttributeError:
57 | charset = "utf-8"
58 | try:
59 | mimetypes = settings.WHITENOISE_MIMETYPES
60 | except AttributeError:
61 | mimetypes = None
62 | try:
63 | add_headers_function = settings.WHITENOISE_ADD_HEADERS_FUNCTION
64 | except AttributeError:
65 | add_headers_function = None
66 | try:
67 | index_file = settings.WHITENOISE_INDEX_FILE
68 | except AttributeError:
69 | index_file = None
70 | try:
71 | immutable_file_test = settings.WHITENOISE_IMMUTABLE_FILE_TEST
72 | except AttributeError:
73 | immutable_file_test = None
74 |
75 | super().__init__(
76 | application=None,
77 | autorefresh=autorefresh,
78 | max_age=max_age,
79 | allow_all_origins=allow_all_origins,
80 | charset=charset,
81 | mimetypes=mimetypes,
82 | add_headers_function=add_headers_function,
83 | index_file=index_file,
84 | immutable_file_test=immutable_file_test,
85 | )
86 |
87 | try:
88 | self.use_finders = settings.WHITENOISE_USE_FINDERS
89 | except AttributeError:
90 | self.use_finders = settings.DEBUG
91 |
92 | try:
93 | self.static_prefix = settings.WHITENOISE_STATIC_PREFIX
94 | except AttributeError:
95 | self.static_prefix = urlparse(settings.STATIC_URL or "").path
96 | script_prefix = get_script_prefix().rstrip("/")
97 | if script_prefix:
98 | if self.static_prefix.startswith(script_prefix):
99 | self.static_prefix = self.static_prefix[len(script_prefix) :]
100 | self.static_prefix = ensure_leading_trailing_slash(self.static_prefix)
101 |
102 | self.static_root = settings.STATIC_ROOT
103 | if self.static_root:
104 | self.add_files(self.static_root, prefix=self.static_prefix)
105 |
106 | try:
107 | root = settings.WHITENOISE_ROOT
108 | except AttributeError:
109 | root = None
110 | if root:
111 | self.add_files(root)
112 |
113 | if self.use_finders and not self.autorefresh:
114 | self.add_files_from_finders()
115 |
116 | def __call__(self, request):
117 | if self.autorefresh:
118 | static_file = self.find_file(request.path_info)
119 | else:
120 | static_file = self.files.get(request.path_info)
121 | if static_file is not None:
122 | return self.serve(static_file, request)
123 | if static_file is None:
124 | if self.static_prefix in request.path_info:
125 | # using path info and static root to check the directory of the recently made webp
126 | raw_full_path = os.path.join(self.static_root, (request.path_info).lstrip("/"))
127 | clean_full_path = raw_full_path.replace("\\", "/")
128 | complete_full_path = clean_full_path.replace(self.static_prefix.rstrip("/"), "", 1)
129 | if os.path.exists(complete_full_path):
130 | self.add_files(self.static_root, prefix=self.static_prefix)
131 | static_file = self.files.get(complete_full_path)
132 | if static_file is None:
133 | request.not_serving = request.path_info
134 | # print("and this too", request.__dict__)
135 | return self.serve(static_file, request)
136 | return self.get_response(request)
137 |
138 | @staticmethod
139 | def serve(static_file, request):
140 | try:
141 | response = static_file.get_response(request.method, request.META)
142 | status = int(response.status)
143 | http_response = WhiteNoiseFileResponse(response.file or (), status=status)
144 | # Remove default content-type
145 | del http_response["content-type"]
146 | for key, value in response.headers:
147 | http_response[key] = value
148 | return http_response
149 | except:
150 | http_response = WhiteNoiseFileResponse((), status=500)
151 | http_response['not serving'] = request.not_serving
152 | return http_response
153 |
154 |
155 | def add_files_from_finders(self):
156 | files = {}
157 | for finder in finders.get_finders():
158 | for path, storage in finder.list(None):
159 | prefix = (getattr(storage, "prefix", None) or "").strip("/")
160 | url = "".join(
161 | (
162 | self.static_prefix,
163 | prefix,
164 | "/" if prefix else "",
165 | path.replace("\\", "/"),
166 | )
167 | )
168 | # Use setdefault as only first matching file should be used
169 | files.setdefault(url, storage.path(path))
170 | stat_cache = {path: os.stat(path) for path in files.values()}
171 | for url, path in files.items():
172 | self.add_file_to_dictionary(url, path, stat_cache=stat_cache)
173 |
174 | def candidate_paths_for_url(self, url):
175 | if self.use_finders and url.startswith(self.static_prefix):
176 | path = finders.find(url[len(self.static_prefix) :])
177 | if path:
178 | yield path
179 | paths = super().candidate_paths_for_url(url)
180 | for path in paths:
181 | yield path
182 |
183 | def immutable_file_test(self, path, url):
184 | """
185 | Determine whether given URL represents an immutable file (i.e. a
186 | file with a hash of its contents as part of its name) which can
187 | therefore be cached forever
188 | """
189 | if not url.startswith(self.static_prefix):
190 | return False
191 | name = url[len(self.static_prefix) :]
192 | name_without_hash = self.get_name_without_hash(name)
193 | if name == name_without_hash:
194 | return False
195 | static_url = self.get_static_url(name_without_hash)
196 | # If the static_url function maps the name without hash
197 | # back to the original name, then we know we've got a
198 | # versioned filename
199 | if static_url and basename(static_url) == basename(url):
200 | return True
201 | return False
202 |
203 | def get_name_without_hash(self, filename):
204 | """
205 | Removes the version hash from a filename e.g, transforms
206 | 'css/application.f3ea4bcc2.css' into 'css/application.css'
207 |
208 | Note: this is specific to the naming scheme used by Django's
209 | CachedStaticFilesStorage. You may have to override this if
210 | you are using a different static files versioning system
211 | """
212 | name_with_hash, ext = os.path.splitext(filename)
213 | name = os.path.splitext(name_with_hash)[0]
214 | return name + ext
215 |
216 | def get_static_url(self, name):
217 | try:
218 | return staticfiles_storage.url(name)
219 | except ValueError:
220 | return None
221 |
--------------------------------------------------------------------------------
/tests/testproj/testapp/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/django-master/django-webp/e3acd34e506add342a6801e7562bbebc4a7f3833/tests/testproj/testapp/migrations/__init__.py
--------------------------------------------------------------------------------
/tests/testproj/testapp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/tests/testproj/testapp/templates/django_webp/index.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load webp %}
3 |
4 |
5 |
6 |
static url: {% static 'django_webp/python.png' %}
12 |webp url: {% webp 'django_webp/python.png' %}
15 |