diff --git a/pontoon/base/models/comment.py b/pontoon/base/models/comment.py index c7459258a1..a9c49b2077 100644 --- a/pontoon/base/models/comment.py +++ b/pontoon/base/models/comment.py @@ -5,7 +5,7 @@ from django.utils import timezone from pontoon.base.models.user import User -from pontoon.base.user_utils import gravatar_url, user_banner +from pontoon.base.user_utils import avatar_url, user_banner if TYPE_CHECKING: @@ -45,7 +45,7 @@ def serialize(self, project_contact): "user_banner": user_banner(author, locale, project_contact) if author else "", - "user_gravatar_url_small": gravatar_url(author) if author else "", + "user_gravatar_url_small": avatar_url(author) if author else "", "created_at": self.timestamp.strftime("%b %d, %Y %H:%M"), "date_iso": self.timestamp.isoformat(), "content": self.content, diff --git a/pontoon/base/models/translation.py b/pontoon/base/models/translation.py index e7d4d30678..92055bb4be 100644 --- a/pontoon/base/models/translation.py +++ b/pontoon/base/models/translation.py @@ -17,7 +17,7 @@ from pontoon.base.models.project_locale import ProjectLocale from pontoon.base.models.user import User from pontoon.base.simple_preview import get_simple_preview -from pontoon.base.user_utils import gravatar_url +from pontoon.base.user_utils import avatar_url from pontoon.checks import DB_FORMATS from pontoon.checks.utils import save_failed_checks @@ -99,7 +99,7 @@ def authors(self): "email": user.email, "display_name": user.name_or_email, "id": user.id, - "gravatar_url": gravatar_url(user), + "gravatar_url": avatar_url(user), "translation_count": user.translations_count, "role": user.user_role, } diff --git a/pontoon/base/models/user.py b/pontoon/base/models/user.py index 96b2c9157d..b79f8ce9f2 100644 --- a/pontoon/base/models/user.py +++ b/pontoon/base/models/user.py @@ -9,8 +9,8 @@ unread_notifications_display, ) from pontoon.base.user_utils import ( + avatar_url, can_translate_locales, - gravatar_url, manager_for_locales, translator_for_locales, ) @@ -92,7 +92,7 @@ def latest_action(user: User) -> ActionLog | None: AuthUser.add_to_class("display_name_and_email", display_name_and_email) AuthUser.add_to_class("latest_action", latest_action) -AuthUser.add_to_class("gravatar_url", gravatar_url) +AuthUser.add_to_class("avatar_url", avatar_url) AuthUser.add_to_class("translator_for_locales", translator_for_locales) AuthUser.add_to_class("manager_for_locales", manager_for_locales) AuthUser.add_to_class("can_translate_locales", can_translate_locales) diff --git a/pontoon/base/templates/allauth/layouts/base.html b/pontoon/base/templates/allauth/layouts/base.html index 4d2d4245d1..12943d1229 100644 --- a/pontoon/base/templates/allauth/layouts/base.html +++ b/pontoon/base/templates/allauth/layouts/base.html @@ -100,7 +100,7 @@ {% if user.is_authenticated %} diff --git a/pontoon/base/templates/widgets/latest_activity.html b/pontoon/base/templates/widgets/latest_activity.html index bbe0e4ed38..e7f33009ad 100644 --- a/pontoon/base/templates/widgets/latest_activity.html +++ b/pontoon/base/templates/widgets/latest_activity.html @@ -5,7 +5,7 @@ {% set action = latest_activity.type + ' by' %} {% set user = latest_activity.user.name_or_email %} {% set link = url('pontoon.contributors.contributor.username', latest_activity.user.username) %} - {% set avatar = latest_activity.user.gravatar_url() %} + {% set avatar = latest_activity.user.avatar_url() %} {% else %} {% set action = 'imported' %} {% set user = '' %} diff --git a/pontoon/base/templates/widgets/profile.html b/pontoon/base/templates/widgets/profile.html index 44963b706d..c74dcb615f 100644 --- a/pontoon/base/templates/widgets/profile.html +++ b/pontoon/base/templates/widgets/profile.html @@ -3,7 +3,7 @@ {% if user.is_authenticated %} @@ -21,7 +21,7 @@ > diff --git a/pontoon/base/tests/models/test_comment.py b/pontoon/base/tests/models/test_comment.py index aa8133cd5b..911717eb66 100644 --- a/pontoon/base/tests/models/test_comment.py +++ b/pontoon/base/tests/models/test_comment.py @@ -1,6 +1,6 @@ import pytest -from pontoon.base.user_utils import gravatar_url, user_banner +from pontoon.base.user_utils import avatar_url, user_banner from pontoon.test.factories import ProjectFactory @@ -14,7 +14,7 @@ def test_serialize_comments(comment_a, team_comment_a): "user_banner": user_banner( comment_a.author, comment_a.translation.locale, project.contact ), - "user_gravatar_url_small": gravatar_url(comment_a.author), + "user_gravatar_url_small": avatar_url(comment_a.author), "created_at": comment_a.timestamp.strftime("%b %d, %Y %H:%M"), "date_iso": comment_a.timestamp.isoformat(), "content": comment_a.content, @@ -28,7 +28,7 @@ def test_serialize_comments(comment_a, team_comment_a): "user_banner": user_banner( team_comment_a.author, team_comment_a.locale, project.contact ), - "user_gravatar_url_small": gravatar_url(team_comment_a.author), + "user_gravatar_url_small": avatar_url(team_comment_a.author), "created_at": team_comment_a.timestamp.strftime("%b %d, %Y %H:%M"), "date_iso": team_comment_a.timestamp.isoformat(), "content": team_comment_a.content, diff --git a/pontoon/base/tests/test_user_utils.py b/pontoon/base/tests/test_user_utils.py index 52b081e7a5..87c80e9bd3 100644 --- a/pontoon/base/tests/test_user_utils.py +++ b/pontoon/base/tests/test_user_utils.py @@ -1,7 +1,15 @@ import pytest +from allauth.socialaccount.models import SocialAccount + from pontoon.base.models.user import User -from pontoon.base.user_utils import user_banner, user_locale_role, user_role +from pontoon.base.user_utils import ( + avatar_url, + fxa_avatar_url, + user_banner, + user_locale_role, + user_role, +) @pytest.mark.django_db @@ -91,3 +99,90 @@ def test_user_banner(user_a, user_b, user_c, user_d, gt_user, locale_a, project_ # System user (Google Translate) project_contact = gt_user assert user_banner(gt_user, locale_a, project_contact)[1] == "" + + +@pytest.mark.django_db +def test_gravatar_url_returns_fxa_avatar_when_linked(user_a): + SocialAccount.objects.create( + user=user_a, + provider="fxa", + uid="1234", + extra_data={"avatar": "https://profile.accounts.firefox.com/v1/avatar/abc"}, + ) + assert avatar_url(user_a) == "https://profile.accounts.firefox.com/v1/avatar/abc" + + +@pytest.mark.django_db +def test_gravatar_url_falls_back_to_gravatar_when_no_fxa(user_a): + url = avatar_url(user_a) + assert "gravatar.com/avatar/" in url + + +@pytest.mark.django_db +def test_gravatar_url_falls_back_to_gravatar_when_fxa_has_no_avatar(user_a): + SocialAccount.objects.create( + user=user_a, + provider="fxa", + uid="1234", + extra_data={}, + ) + url = avatar_url(user_a) + assert "gravatar.com/avatar/" in url + + +@pytest.mark.django_db +def test_fxa_avatar_returns_none_for_unsaved_user(): + user = User(username="unsaved", email="unsaved@example.com") + assert fxa_avatar_url(user) is None + + +@pytest.mark.django_db +def test_fxa_avatar_returns_url_from_db(user_a): + SocialAccount.objects.create( + user=user_a, + provider="fxa", + uid="1234", + extra_data={"avatar": "https://profile.accounts.firefox.com/v1/avatar/abc"}, + ) + assert ( + fxa_avatar_url(user_a) == "https://profile.accounts.firefox.com/v1/avatar/abc" + ) + + +@pytest.mark.django_db +def test_fxa_avatar_returns_none_when_no_fxa_account(user_a): + assert fxa_avatar_url(user_a) is None + + +@pytest.mark.django_db +def test_fxa_avatar_returns_none_when_fxa_has_no_avatar(user_a): + SocialAccount.objects.create( + user=user_a, + provider="fxa", + uid="1234", + extra_data={}, + ) + assert fxa_avatar_url(user_a) is None + + +@pytest.mark.django_db +def test_fxa_avatar_uses_prefetched_accounts(user_a): + account = SocialAccount( + user=user_a, + provider="fxa", + uid="1234", + extra_data={ + "avatar": "https://profile.accounts.firefox.com/v1/avatar/prefetched" + }, + ) + user_a._prefetched_fxa_accounts = [account] + assert ( + fxa_avatar_url(user_a) + == "https://profile.accounts.firefox.com/v1/avatar/prefetched" + ) + + +@pytest.mark.django_db +def test_fxa_avatar_uses_prefetched_accounts_when_empty(user_a): + user_a._prefetched_fxa_accounts = [] + assert fxa_avatar_url(user_a) is None diff --git a/pontoon/base/user_utils.py b/pontoon/base/user_utils.py index 70d7c2d3f5..b583f86d94 100644 --- a/pontoon/base/user_utils.py +++ b/pontoon/base/user_utils.py @@ -20,9 +20,29 @@ def is_system_user(user: User) -> bool: return user.pk is None or user.profile.system_user -def gravatar_url(user: User, size: int = 88) -> str: - email = md5(user.email.lower().encode("utf-8")).hexdigest() +def fxa_avatar_url(user: User) -> str | None: + if user.pk is None: + return None + + if hasattr(user, "_prefetched_fxa_accounts") and isinstance( + user._prefetched_fxa_accounts, list + ): + return ( + user._prefetched_fxa_accounts[0].extra_data.get("avatar") + if user._prefetched_fxa_accounts + else None + ) + + fxa = user.socialaccount_set.filter(provider="fxa").first() + return fxa.extra_data.get("avatar") if fxa else None + +def avatar_url(user: User, size: int = 88) -> str: + + if fxa_avatar := fxa_avatar_url(user): + return fxa_avatar + + email = md5(user.email.lower().encode("utf-8")).hexdigest() name = quote(user.display_name) background = "333941" color = "FFFFFF" @@ -42,7 +62,7 @@ def user_serialize(user: User): """Serialize Project contact""" return { - "avatar": gravatar_url(user), + "avatar": avatar_url(user), "name": user.name_or_email, "url": profile_url(user), } diff --git a/pontoon/base/views.py b/pontoon/base/views.py index ebd94cb6b3..19c7f5c312 100755 --- a/pontoon/base/views.py +++ b/pontoon/base/views.py @@ -6,6 +6,8 @@ from datetime import datetime from urllib.parse import urlparse +from allauth.socialaccount.models import SocialAccount + from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -50,10 +52,10 @@ from pontoon.base.services import readonly_exists from pontoon.base.templatetags.helpers import provider_login_url from pontoon.base.user_utils import ( + avatar_url, can_manage_locales, can_translate, can_translate_locales, - gravatar_url, manager_for_locales, profile_url, translated_projects, @@ -472,7 +474,16 @@ def get_translation_history(request): "timestamp" ), ), - "user", + Prefetch( + "user", + queryset=User.objects.prefetch_related( + Prefetch( + "socialaccount_set", + queryset=SocialAccount.objects.filter(provider="fxa"), + to_attr="_prefetched_fxa_accounts", + ) + ), + ), "approved_user", "rejected_user", "errors", @@ -492,7 +503,7 @@ def get_translation_history(request): "user": u.name_or_email, "uid": u.pk, "username": u.username, - "user_gravatar_url_small": gravatar_url(u), + "user_gravatar_url_small": avatar_url(u), "user_banner": user_banner(u, locale, project_contact), "date": t.date, "approved_user": t.approved_user.name_or_email @@ -534,6 +545,13 @@ def get_team_comments(request): comments = ( Comment.objects.filter(entity=entity) .filter(Q(locale=locale) | Q(pinned=True)) + .prefetch_related( + Prefetch( + "author__socialaccount_set", + queryset=SocialAccount.objects.filter(provider="fxa"), + to_attr="_prefetched_fxa_accounts", + ) + ) .order_by("timestamp") ) @@ -840,6 +858,13 @@ def get_users(request): .exclude(email__regex=r"^deleted-user-(\w+)@example.com$") # Prefetch profile for retrieving username .prefetch_related("profile") + .prefetch_related( + Prefetch( + "socialaccount_set", + queryset=SocialAccount.objects.filter(provider="fxa"), + to_attr="_prefetched_fxa_accounts", + ) + ) .annotate( in_locale=Count( "translation", filter=Q(translation__locale__code=locale_code) @@ -856,7 +881,7 @@ def get_users(request): for u in users: payload.append( { - "gravatar": gravatar_url(u, 44), + "gravatar": avatar_url(u, 44), "name": u.name_or_email, "url": profile_url(u), "username": u.profile.username, @@ -1071,8 +1096,8 @@ def user_data(request): "tour_status": user.profile.tour_status, "has_dismissed_addon_promotion": user.profile.has_dismissed_addon_promotion, "logout_url": logout_url, - "gravatar_url_small": gravatar_url(user, 88), - "gravatar_url_big": gravatar_url(user, 176), + "gravatar_url_small": avatar_url(user, 88), + "gravatar_url_big": avatar_url(user, 176), "notifications": serialized_notifications(user), "theme": user.profile.theme, } diff --git a/pontoon/contributors/templates/contributors/contributors.html b/pontoon/contributors/templates/contributors/contributors.html index 117624ad64..1172977cd7 100644 --- a/pontoon/contributors/templates/contributors/contributors.html +++ b/pontoon/contributors/templates/contributors/contributors.html @@ -60,7 +60,7 @@

> diff --git a/pontoon/contributors/templates/contributors/profile.html b/pontoon/contributors/templates/contributors/profile.html index eafe9e6c94..0c8ecba89f 100644 --- a/pontoon/contributors/templates/contributors/profile.html +++ b/pontoon/contributors/templates/contributors/profile.html @@ -48,7 +48,7 @@ {% endif %} diff --git a/pontoon/contributors/templates/contributors/settings.html b/pontoon/contributors/templates/contributors/settings.html index 0bd67cea8a..9d16aa7d2f 100644 --- a/pontoon/contributors/templates/contributors/settings.html +++ b/pontoon/contributors/templates/contributors/settings.html @@ -14,7 +14,7 @@
Update profile picture
diff --git a/pontoon/contributors/templates/contributors/widgets/contributor_list.html b/pontoon/contributors/templates/contributors/widgets/contributor_list.html index 0dc2e84b1e..d2fb2fd94e 100644 --- a/pontoon/contributors/templates/contributors/widgets/contributor_list.html +++ b/pontoon/contributors/templates/contributors/widgets/contributor_list.html @@ -29,7 +29,7 @@ > diff --git a/pontoon/contributors/utils.py b/pontoon/contributors/utils.py index 6a1ef4fe21..6b87543d6c 100644 --- a/pontoon/contributors/utils.py +++ b/pontoon/contributors/utils.py @@ -6,6 +6,7 @@ import jwt +from allauth.socialaccount.models import SocialAccount from dateutil.relativedelta import relativedelta from django.conf import settings @@ -103,10 +104,20 @@ def users_with_translations_counts( for user in loc.translators_group.fetched_translators: translators[user].add(loc.code) - contributors = User.objects.filter( - pk__in=user_stats.keys(), - is_active=True, - ).prefetch_related("profile") + contributors = ( + User.objects.filter( + pk__in=user_stats.keys(), + is_active=True, + ) + .prefetch_related("profile") + .prefetch_related( + Prefetch( + "socialaccount_set", + queryset=SocialAccount.objects.filter(provider="fxa"), + to_attr="_prefetched_fxa_accounts", + ) + ) + ) if None in user_stats.keys(): contributors = list(contributors) diff --git a/pontoon/messaging/templates/messaging/includes/sent.html b/pontoon/messaging/templates/messaging/includes/sent.html index 3d4944f713..3ddf34ccbd 100644 --- a/pontoon/messaging/templates/messaging/includes/sent.html +++ b/pontoon/messaging/templates/messaging/includes/sent.html @@ -15,7 +15,7 @@ rel="noopener noreferrer" >