Use 404 image when no image is available
All checks were successful
Deploy to Server / deploy (push) Successful in 10s
All checks were successful
Deploy to Server / deploy (push) Successful in 10s
This commit is contained in:
parent
6c22559fb5
commit
768d986556
5 changed files with 88 additions and 6 deletions
|
|
@ -103,7 +103,7 @@ MEDIA_URL = "/media/"
|
||||||
|
|
||||||
STATIC_ROOT: Path = DATA_DIR / "staticfiles"
|
STATIC_ROOT: Path = DATA_DIR / "staticfiles"
|
||||||
STATIC_ROOT.mkdir(exist_ok=True)
|
STATIC_ROOT.mkdir(exist_ok=True)
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "/static/"
|
||||||
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
|
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
|
||||||
|
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "UTC"
|
||||||
|
|
|
||||||
8
static/404.svg
Normal file
8
static/404.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<rect width="100%" height="100%" fill="#9146ff" />
|
||||||
|
<g fill="#fff" font-family="Arial, Helvetica, sans-serif" font-size="24" font-weight="700">
|
||||||
|
<text x="50%" y="45%" dominant-baseline="middle" text-anchor="middle">:(</text>
|
||||||
|
<text x="50%" y="60%" dominant-baseline="middle" text-anchor="middle">404</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
|
|
@ -1,8 +1,38 @@
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.db.models.fields.files import FieldFile
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
class TwitchConfig(AppConfig):
|
class TwitchConfig(AppConfig):
|
||||||
"""AppConfig subclass for the 'twitch' application."""
|
"""Django app configuration for the Twitch app."""
|
||||||
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "twitch"
|
name = "twitch"
|
||||||
|
|
||||||
|
def ready(self) -> None: # noqa: D102
|
||||||
|
logger: logging.Logger = logging.getLogger("ttvdrops.apps")
|
||||||
|
|
||||||
|
# Patch FieldFile.open to swallow FileNotFoundError and provide
|
||||||
|
# an empty in-memory file-like object so image dimension
|
||||||
|
# calculations don't crash when the on-disk file was removed.
|
||||||
|
try:
|
||||||
|
orig_open: Callable[..., FieldFile] = FieldFile.open
|
||||||
|
|
||||||
|
def _safe_open(self: FieldFile, mode: str = "rb") -> FieldFile:
|
||||||
|
try:
|
||||||
|
return orig_open(self, mode)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Provide an empty BytesIO so subsequent dimension checks
|
||||||
|
# read harmlessly and return (None, None).
|
||||||
|
self._file = io.BytesIO(b"") # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
return self
|
||||||
|
|
||||||
|
FieldFile.open = _safe_open
|
||||||
|
except (AttributeError, TypeError) as exc:
|
||||||
|
logger.debug("Failed to patch FieldFile.open: %s", exc)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"""Custom template tags for rendering responsive images with modern formats."""
|
import logging
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ if TYPE_CHECKING:
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
logger = logging.getLogger("ttvdrops.image_tags")
|
||||||
|
|
||||||
|
|
||||||
def get_format_url(image_url: str, fmt: str) -> str:
|
def get_format_url(image_url: str, fmt: str) -> str:
|
||||||
|
|
@ -70,6 +72,18 @@ def picture( # noqa: PLR0913, PLR0917
|
||||||
if not src:
|
if not src:
|
||||||
return SafeString("")
|
return SafeString("")
|
||||||
|
|
||||||
|
# If the src points to a local MEDIA file but the underlying file is
|
||||||
|
# missing on disk, replace with a small static fallback to avoid
|
||||||
|
# raising FileNotFoundError during template rendering.
|
||||||
|
try:
|
||||||
|
media_url: str = settings.MEDIA_URL or "/media/"
|
||||||
|
if src.startswith(media_url):
|
||||||
|
name: str = src[len(media_url) :].lstrip("/")
|
||||||
|
if not default_storage.exists(name):
|
||||||
|
src = "/static/404.svg"
|
||||||
|
except (ValueError, OSError) as exc:
|
||||||
|
logger.debug("Error while resolving media file %s: %s", src, exc)
|
||||||
|
|
||||||
# For Twitch CDN URLs, skip format conversion and use simple img tag
|
# For Twitch CDN URLs, skip format conversion and use simple img tag
|
||||||
if "static-cdn.jtvnw.net" in src:
|
if "static-cdn.jtvnw.net" in src:
|
||||||
return format_html(
|
return format_html(
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,43 @@
|
||||||
"""Tests for custom image template tags."""
|
|
||||||
|
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.template import Template
|
from django.template import Template
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
|
from twitch.models import Game
|
||||||
from twitch.templatetags.image_tags import get_format_url
|
from twitch.templatetags.image_tags import get_format_url
|
||||||
from twitch.templatetags.image_tags import picture
|
from twitch.templatetags.image_tags import picture
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/")
|
||||||
|
class ImageTagsTests(SimpleTestCase):
|
||||||
|
"""Tests for image template tags and related functionality."""
|
||||||
|
|
||||||
|
def test_picture_empty_src_returns_empty(self) -> None:
|
||||||
|
"""Test that picture tag with empty src returns empty string."""
|
||||||
|
result = picture("")
|
||||||
|
assert not str(result)
|
||||||
|
|
||||||
|
def test_picture_keeps_external_url(self) -> None:
|
||||||
|
"""Test that picture tag does not modify external URLs and does not attempt format conversion."""
|
||||||
|
src = "https://example.com/images/sample.png"
|
||||||
|
result: SafeString = picture(src, alt="alt", width=16, height=16)
|
||||||
|
rendered = str(result)
|
||||||
|
|
||||||
|
# Should still contain the original external URL
|
||||||
|
assert src in rendered
|
||||||
|
|
||||||
|
def test_model_init_with_missing_image_does_not_raise(self) -> None:
|
||||||
|
"""Test that initializing a model with a missing image file does not raise an error."""
|
||||||
|
# Simulate a Game instance with a missing local image file. The
|
||||||
|
# AppConfig.ready() wrapper should prevent FileNotFoundError during
|
||||||
|
# model initialization.
|
||||||
|
g = Game(twitch_id="test-game", box_art_file="campaigns/images/missing.png")
|
||||||
|
# If initialization reached this point without raising, we consider
|
||||||
|
# the protection successful.
|
||||||
|
assert g.twitch_id == "test-game"
|
||||||
|
|
||||||
|
|
||||||
class TestGetFormatUrl:
|
class TestGetFormatUrl:
|
||||||
"""Tests for the get_format_url helper function."""
|
"""Tests for the get_format_url helper function."""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue