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