diff --git a/templates/misc/avatar.part.html b/templates/misc/avatar.part.html
new file mode 100644
index 0000000000..b653d14fb3
--- /dev/null
+++ b/templates/misc/avatar.part.html
@@ -0,0 +1,11 @@
+{% load captureas %}
+
+{# Template used by the templatetag "avatar" defined in zds/utils/templatetags/profile.py #}
+
+{% captureas alt_text %}Avatar de {{ username }}{% endcaptureas %}
+
+{% if avatar_url %}
+
+{% endif %}
diff --git a/templates/misc/member_item.part.html b/templates/misc/member_item.part.html
index 4c3ca23da7..e6e3fcc12e 100644
--- a/templates/misc/member_item.part.html
+++ b/templates/misc/member_item.part.html
@@ -16,7 +16,7 @@
{% endif %}>
{% if avatar %}
-
diff --git a/templates/misc/message_user.html b/templates/misc/message_user.html
index adf8ac626d..37401bfe50 100644
--- a/templates/misc/message_user.html
+++ b/templates/misc/message_user.html
@@ -5,7 +5,7 @@
{% with profile=member|profile %}
-
+ {% avatar profile %}
{% include 'misc/badge.part.html' %}
diff --git a/yarn.lock b/yarn.lock
index d90bcf6070..ef1df2fa67 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1049,6 +1049,13 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz#da06b79c3d9c3d9958eb307aa832ac68ead79bee"
integrity sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==
+canvas-renderer@~2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/canvas-renderer/-/canvas-renderer-2.2.1.tgz#c1d131f78a9799aca8af9679ad0a005052b65550"
+ integrity sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg==
+ dependencies:
+ "@types/node" "*"
+
capture-stack-trace@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz#1c43f6b059d4249e7f3f8724f15f048b927d3a8a"
@@ -4809,6 +4816,13 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"
+jdenticon@3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/jdenticon/-/jdenticon-3.3.0.tgz#64bae9f9b3cf5c2a210e183648117afe3a89b367"
+ integrity sha512-DhuBRNRIybGPeAjMjdHbkIfiwZCCmf8ggu7C49jhp6aJ7DYsZfudnvnTY5/1vgUhrGA7JaDAx1WevnpjCPvaGg==
+ dependencies:
+ canvas-renderer "~2.2.0"
+
jpeg-js@0.0.4:
version "0.0.4"
resolved "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.0.4.tgz"
diff --git a/zds/member/api/serializers.py b/zds/member/api/serializers.py
index 0f8b614e35..8fa9d7d9cf 100644
--- a/zds/member/api/serializers.py
+++ b/zds/member/api/serializers.py
@@ -16,7 +16,7 @@ class UserListSerializer(serializers.ModelSerializer):
serializers.
"""
- avatar_url = serializers.CharField(source="profile.get_avatar_url")
+ avatar_url = serializers.CharField(source="profile.get_absolute_avatar_url")
html_url = serializers.CharField(source="get_absolute_url")
class Meta:
@@ -34,7 +34,7 @@ class ProfileListSerializer(serializers.ModelSerializer):
html_url = serializers.CharField(source="user.get_absolute_url")
is_active = serializers.BooleanField(source="user.is_active")
date_joined = serializers.DateTimeField(source="user.date_joined")
- avatar_url = serializers.CharField(source="get_avatar_url")
+ avatar_url = serializers.CharField(source="get_absolute_avatar_url")
permissions = DRYPermissionsField(additional_actions=["ban"])
class Meta:
@@ -78,7 +78,7 @@ class ProfileDetailSerializer(serializers.ModelSerializer):
email = serializers.EmailField(source="user.email")
is_active = serializers.BooleanField(source="user.is_active")
date_joined = serializers.DateTimeField(source="user.date_joined")
- avatar_url = serializers.CharField(source="get_avatar_url")
+ avatar_url = serializers.CharField(source="get_absolute_avatar_url")
permissions = DRYPermissionsField(additional_actions=["ban"])
class Meta:
diff --git a/zds/member/api/tests.py b/zds/member/api/tests.py
index 9160869e7b..20103d34bc 100644
--- a/zds/member/api/tests.py
+++ b/zds/member/api/tests.py
@@ -400,7 +400,7 @@ def test_detail_of_the_member(self):
self.assertEqual(profile.user.is_active, response.data.get("is_active"))
self.assertIsNotNone(response.data.get("date_joined"))
self.assertEqual(profile.site, response.data.get("site"))
- self.assertEqual(profile.get_avatar_url(), response.data.get("avatar_url"))
+ self.assertEqual(profile.get_absolute_avatar_url(), response.data.get("avatar_url"))
self.assertEqual(profile.biography, response.data.get("biography"))
self.assertEqual(profile.sign, response.data.get("sign"))
self.assertFalse(response.data.get("show_email"))
@@ -442,7 +442,7 @@ def test_detail_of_a_member(self):
self.assertEqual(self.profile.user.is_active, response.data.get("is_active"))
self.assertIsNotNone(response.data.get("date_joined"))
self.assertEqual(self.profile.site, response.data.get("site"))
- self.assertEqual(self.profile.get_avatar_url(), response.data.get("avatar_url"))
+ self.assertEqual(self.profile.get_absolute_avatar_url(), response.data.get("avatar_url"))
self.assertEqual(self.profile.biography, response.data.get("biography"))
self.assertEqual(self.profile.sign, response.data.get("sign"))
self.assertFalse(response.data.get("show_email"))
diff --git a/zds/member/forms.py b/zds/member/forms.py
index 1002d5fd81..b7b2a62899 100644
--- a/zds/member/forms.py
+++ b/zds/member/forms.py
@@ -194,9 +194,7 @@ class MiniProfileForm(forms.Form):
label="Avatar",
required=False,
max_length=Profile._meta.get_field("avatar_url").max_length,
- widget=forms.TextInput(
- attrs={"placeholder": _("Lien vers un avatar externe (laissez vide pour utiliser Gravatar).")}
- ),
+ widget=forms.TextInput(attrs={"placeholder": _("Lien vers un avatar externe.")}),
)
sign = forms.CharField(
diff --git a/zds/member/management/commands/migrate_from_gravatar.py b/zds/member/management/commands/migrate_from_gravatar.py
new file mode 100644
index 0000000000..2c4890fa1d
--- /dev/null
+++ b/zds/member/management/commands/migrate_from_gravatar.py
@@ -0,0 +1,29 @@
+from hashlib import md5
+from time import sleep
+
+import requests
+from django.core.management.base import BaseCommand
+from django.db.models import Q
+
+from zds.member.models import Profile
+
+
+class Command(BaseCommand):
+ help = "Migrate from Gravatar"
+
+ def handle(self, *args, **options):
+ # We have profiles with either NULL or empty avatar_url field
+ profiles_without_avatar_url = Profile.objects.filter(Q(avatar_url__isnull=True) | Q(avatar_url=""))
+ total = profiles_without_avatar_url.count()
+ i = 1
+ for profile in profiles_without_avatar_url.iterator():
+ hash = md5(profile.user.email.lower().encode("utf-8")).hexdigest()
+ gravatar_url = f"https://secure.gravatar.com/avatar/{hash}"
+ r = requests.get(f"{gravatar_url}?d=404")
+ if r.status_code == 200:
+ profile.avatar_url = f"{gravatar_url}?s=200"
+ profile.save()
+ self.stdout.write(f"\rProgress: {i}/{total}", ending="")
+ i += 1
+ sleep(1)
+ self.stdout.write(self.style.SUCCESS("\nSuccessfully migrated from Gravatar!"))
diff --git a/zds/member/models.py b/zds/member/models.py
index 856e844404..53fefa63d9 100644
--- a/zds/member/models.py
+++ b/zds/member/models.py
@@ -105,22 +105,14 @@ def get_city(self):
return geo_location
- def get_avatar_url(self, size=80):
- """Get the avatar URL for this profile.
- If the user has defined a custom URL, use it.
- If not, use Gravatar.
- :return: The avatar URL for this profile
- :rtype: str
- """
- if self.avatar_url:
- if self.avatar_url.startswith(settings.MEDIA_URL):
- return "{}{}".format(settings.ZDS_APP["site"]["url"], self.avatar_url)
- else:
- return self.avatar_url
- else:
- return "https://secure.gravatar.com/avatar/{}?d=identicon&s={}".format(
- md5(self.user.email.lower().encode("utf-8")).hexdigest(), size
- )
+ def get_absolute_avatar_url(self):
+ """Gets the avatar URL of this profile.
+ :return: The absolute URL of this profile's avatar
+ :rtype: str or None
+ """
+ if self.avatar_url and self.avatar_url.startswith(settings.MEDIA_URL):
+ return settings.ZDS_APP["site"]["url"] + self.avatar_url
+ return self.avatar_url
def get_post_count(self):
"""
diff --git a/zds/member/tests/tests_models.py b/zds/member/tests/tests_models.py
index c21c30b114..2a74b4e608 100644
--- a/zds/member/tests/tests_models.py
+++ b/zds/member/tests/tests_models.py
@@ -29,22 +29,23 @@ def setUp(self):
def test_get_absolute_url_for_details_of_member(self):
self.assertEqual(self.user1.get_absolute_url(), f"/@{self.user1.user.username}")
- def test_get_avatar_url(self):
- # if no url was specified -> gravatar !
- self.assertIn("gravatar.com", self.user1.get_avatar_url())
+ def test_get_absolute_avatar_url(self):
+ # if no url was specified -> nothing !
+ self.assertEqual(self.user1.get_absolute_avatar_url(), None)
# if an url is specified -> take it !
user2 = ProfileFactory()
testurl = "http://test.com/avatar.jpg"
user2.avatar_url = testurl
- self.assertEqual(user2.get_avatar_url(), testurl)
+ self.assertEqual(user2.get_absolute_avatar_url(), testurl)
# if url is relative, send absolute url
- gallerie_avtar = GalleryFactory()
- image_avatar = ImageFactory(gallery=gallerie_avtar)
+ gallerie_avatar = GalleryFactory()
+ image_avatar = ImageFactory(gallery=gallerie_avatar)
user2.avatar_url = image_avatar.physical.url
- self.assertNotEqual(user2.get_avatar_url(), image_avatar.physical.url)
- self.assertIn("http", user2.get_avatar_url())
+ self.assertNotEqual(user2.get_absolute_avatar_url(), image_avatar.physical.url)
+ self.assertIn(image_avatar.physical.url, user2.get_absolute_avatar_url())
+ self.assertIn("http", user2.get_absolute_avatar_url())
def test_get_post_count(self):
# Start with 0
diff --git a/zds/mp/api/tests.py b/zds/mp/api/tests.py
index 25b065d0b3..7a0290dcf1 100644
--- a/zds/mp/api/tests.py
+++ b/zds/mp/api/tests.py
@@ -211,7 +211,7 @@ def test_expand_list_of_private_topics_for_author(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
author = response.data.get("results")[0].get("author")
self.assertEqual(author.get("username"), self.profile.user.username)
- self.assertEqual(author.get("avatar_url"), self.profile.get_avatar_url())
+ self.assertEqual(author.get("avatar_url"), self.profile.get_absolute_avatar_url())
def test_create_private_topics_with_client_unauthenticated(self):
"""
diff --git a/zds/utils/templatetags/profile.py b/zds/utils/templatetags/profile.py
index bf234cc1b6..87bd6f7283 100644
--- a/zds/utils/templatetags/profile.py
+++ b/zds/utils/templatetags/profile.py
@@ -5,7 +5,6 @@
from django.core.cache import cache
from zds.member.models import Profile
-from zds.utils.templatetags.remove_url_scheme import remove_url_scheme
register = template.Library()
@@ -73,13 +72,11 @@ def state(current_user):
return user_state
-@register.simple_tag
-def avatar_url(profile: Profile, size=80) -> str:
- """
- Return the URL of the avatar of a profile.
- If the profile is None, return an empty string.
- """
+@register.inclusion_tag("misc/avatar.part.html")
+def avatar(profile: Profile, size=80) -> dict:
if profile is not None:
- url = profile.get_avatar_url(size)
- return remove_url_scheme(url)
- return ""
+ return {
+ "avatar_url": profile.avatar_url,
+ "avatar_size": size,
+ "username": profile.user.username,
+ }