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

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4from __future__ import annotations 

5 

6import hashlib 

7import os.path 

8from ssl import CertificateError 

9from typing import TYPE_CHECKING, Literal 

10from urllib.parse import urlencode 

11 

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 

18 

19from weblate.utils.errors import report_error 

20from weblate.utils.requests import request 

21 

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 

24 

25 

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" 

31 

32 mail_hash = hashlib.md5(email.lower().encode(), usedforsecurity=False).hexdigest() 

33 

34 querystring = urlencode({"d": settings.AVATAR_DEFAULT_IMAGE, "s": str(size)}) 

35 

36 return f"{settings.AVATAR_URL_PREFIX}avatar/{mail_hash}?{querystring}" 

37 

38 

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") 

42 

43 

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() 

52 

53 

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))) 

58 

59 # Try using avatar specific cache if available 

60 try: 

61 cache = caches["avatar"] 

62 except InvalidCacheBackendError: 

63 cache = caches["default"] 

64 

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) 

73 

74 return image 

75 

76 

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 

82 

83 

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() 

96 

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 

103 

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}) 

110 

111 username = format_html( 

112 '<img src="{}" class="avatar w32" loading="lazy" alt="{}" /> {}', 

113 avatar, 

114 gettext("User avatar"), 

115 username, 

116 ) 

117 

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)