Coverage for app/venv/lib/python3.14/site-packages/weblate/accounts/avatar.py: 53%
70 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
4from __future__ import annotations
6import hashlib
7import os.path
8from ssl import CertificateError
9from typing import TYPE_CHECKING, Literal
10from urllib.parse import urlencode
12from django.conf import settings
13from django.contrib.staticfiles import finders
14from django.core.cache import InvalidCacheBackendError, caches
15from django.urls import reverse
16from django.utils.html import format_html
17from django.utils.translation import gettext, pgettext
19from weblate.utils.errors import report_error
20from weblate.utils.requests import request
22if TYPE_CHECKING: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 from weblate.auth.models import User
26def avatar_for_email(email, size=80) -> str:
27 """Generate url for avatar."""
28 # Safely handle blank e-mail
29 if not email: 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 email = "noreply@weblate.org"
32 mail_hash = hashlib.md5(email.lower().encode(), usedforsecurity=False).hexdigest()
34 querystring = urlencode({"d": settings.AVATAR_DEFAULT_IMAGE, "s": str(size)})
36 return f"{settings.AVATAR_URL_PREFIX}avatar/{mail_hash}?{querystring}"
39def get_fallback_avatar_url(size: int, name: Literal["weblate", "api"] = "weblate"):
40 """Return URL of fallback avatar."""
41 return os.path.join(settings.STATIC_URL, f"{name}-{size}.png")
44def get_fallback_avatar(size: int):
45 """Return fallback avatar."""
46 filename = finders.find(f"weblate-{size}.png")
47 if filename is None:
48 msg = f"Missing fallback avatar file for {size=}!"
49 raise OSError(msg)
50 with open(filename, "rb") as handle:
51 return handle.read()
54def get_avatar_image(user: User, size: int):
55 """Return avatar image from cache (if available) or download it."""
56 username = user.username
57 cache_key = "-".join(("avatar-img", username, str(size)))
59 # Try using avatar specific cache if available
60 try:
61 cache = caches["avatar"]
62 except InvalidCacheBackendError:
63 cache = caches["default"]
65 image = cache.get(cache_key)
66 if image is None:
67 try:
68 image = download_avatar_image(user.email, size)
69 cache.set(cache_key, image)
70 except (OSError, CertificateError):
71 report_error(f"Could not fetch avatar for {username}")
72 return get_fallback_avatar(size)
74 return image
77def download_avatar_image(email: str, size: int):
78 """Download avatar image from remote server."""
79 url = avatar_for_email(email, size)
80 response = request("get", url, timeout=1.0)
81 return response.content
84def get_user_display(user: User, icon: bool = True, link: bool = False):
85 """Nicely format user for display."""
86 # Did we get any user?
87 if user is None: 87 ↛ 89line 87 didn't jump to line 89 because the condition on line 87 was never true
88 # None user, probably remotely triggered action
89 username = full_name = pgettext("No known user", "None")
90 email = "noreply@weblate.org"
91 else:
92 # Get basic info
93 username = user.username
94 email = user.email
95 full_name = user.full_name.strip()
97 if not full_name: 97 ↛ 99line 97 didn't jump to line 99 because the condition on line 97 was never true
98 # Use user name if full name is empty
99 full_name = username
100 elif username == email: 100 ↛ 102line 100 didn't jump to line 102 because the condition on line 100 was never true
101 # Use full name in case username matches e-mail
102 username = full_name
104 # Icon requested?
105 if icon and settings.ENABLE_AVATARS: 105 ↛ 118line 105 didn't jump to line 118 because the condition on line 105 was always true
106 if email == "noreply@weblate.org": 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 avatar = get_fallback_avatar_url(32)
108 else:
109 avatar = reverse("user_avatar", kwargs={"user": user.username, "size": 32})
111 username = format_html(
112 '<img src="{}" class="avatar w32" loading="lazy" alt="{}" /> {}',
113 avatar,
114 gettext("User avatar"),
115 username,
116 )
118 if link and user is not None: 118 ↛ 125line 118 didn't jump to line 125 because the condition on line 118 was always true
119 return format_html(
120 '<a href="{}" title="{}" class="user-link"><span>{}</span></a>',
121 user.get_absolute_url(),
122 full_name,
123 username,
124 )
125 return format_html('<span title="{}">{}</span>', full_name, username)