diff --git a/readthedocsext/theme/templates/includes/elements/chips/project.html b/readthedocsext/theme/templates/includes/elements/chips/project.html index 8774d278..55a111f7 100644 --- a/readthedocsext/theme/templates/includes/elements/chips/project.html +++ b/readthedocsext/theme/templates/includes/elements/chips/project.html @@ -14,21 +14,11 @@ {% endblock chip_classes %} {% block chip_icon %} - {% if project.remote_repository %} - - {% else %} - {% comment %} - For now, this is using Dicebear, which has a few random generators for - user/project icons. We don't need to use Dicebear, but could use this or - similar to generate files on disk and use an md5 hash or similar to link - to the same image always. - {% endcomment %} - - {% endif %} + {% endblock chip_icon %} - + {% block chip_text %} - {{ project.name }} + {{ project.name }} {% endblock chip_text %} {% block chip_detail_text %} @@ -39,21 +29,11 @@ {% endblock popupcard_image %} {% block popupcard_header %} - {{ project.name }} + {{ project.name }} {% endblock popupcard_header %} {% block popupcard_right %} - {% if project.remote_repository %} - - {% else %} - {% comment %} - For now, this is using Dicebear, which has a few random generators for - user/project icons. We don't need to use Dicebear, but could use this or - similar to generate files on disk and use an md5 hash or similar to link - to the same image always. - {% endcomment %} - - {% endif %} + {% endblock popupcard_right %} {% block popupcard_meta %} @@ -63,7 +43,7 @@ {% block popupcard_content %}
- +
diff --git a/readthedocsext/theme/templates/projects/includes/project_image.html b/readthedocsext/theme/templates/projects/includes/project_image.html index ff434c2f..5f5a8672 100644 --- a/readthedocsext/theme/templates/projects/includes/project_image.html +++ b/readthedocsext/theme/templates/projects/includes/project_image.html @@ -5,8 +5,4 @@ to the same image always. {% endcomment %} -{% if project.remote_repository %} - -{% else %} - -{% endif %} + diff --git a/readthedocsext/theme/templates/theme/images/avatar.svg b/readthedocsext/theme/templates/theme/images/avatar.svg new file mode 100644 index 00000000..c1c9bcd1 --- /dev/null +++ b/readthedocsext/theme/templates/theme/images/avatar.svg @@ -0,0 +1,25 @@ + + "Initials" by "Florian Körner", licensed under "CC0 1.0". / Remix of Initials avatar from dicebear.com + + + + Initials + + + Florian Körner + + + https://github.com/dicebear/dicebear + + + + + + + + + + + {{ letters }} + + diff --git a/readthedocsext/theme/urls.py b/readthedocsext/theme/urls.py new file mode 100644 index 00000000..da5e3689 --- /dev/null +++ b/readthedocsext/theme/urls.py @@ -0,0 +1,14 @@ +# pylint: disable=missing-docstring + +from django.urls import re_path +from readthedocs.constants import pattern_opts + +from .views import AvatarImageProjectView + +urlpatterns = [ + re_path( + r"avatar/project/(?P{project_slug})/$".format(**pattern_opts), + AvatarImageProjectView.as_view(), + name="theme_avatar_project", + ), +] diff --git a/readthedocsext/theme/views.py b/readthedocsext/theme/views.py new file mode 100644 index 00000000..85c42a68 --- /dev/null +++ b/readthedocsext/theme/views.py @@ -0,0 +1,108 @@ +import random + +from django.http.response import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.views.generic.base import TemplateView +from readthedocs.projects.models import Project + + +class AvatarImageBaseView(TemplateView): + + """ + Base view for project, organization, team or user avatars. + """ + + http_method_names = ["get", "head", "options"] + + template_name = "theme/images/avatar.svg" + content_type = "image/svg+xml" + + # Primary to secondary color gradient, generated using a gradient generator + COLORS = [ + "#0993af", + "#0090b7", + "#008bbe", + "#0087c5", + "#0081cb", + "#007bcf", + "#1d73d1", + "#446bd0", + "#6060cc", + "#7854c5", + ] + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + remote_avatar_url = self.get_remote_avatar_url() + if remote_avatar_url: + return HttpResponseRedirect(remote_avatar_url) + else: + context = self.get_context_data() + return self.render_to_response(context) + + def get_object(self): + raise NotImplementedError + + def get_queryset(self): + raise NotImplementedError + + def get_remote_avatar_url(self): + raise NotImplementedError + + def get_avatar_color(self): + random.seed(self.object.pk) + return random.choice(self.COLORS) + + def get_avatar_letters(self): + raise NotImplementedError + + def get_context_data(self): + # Truncate letters, use a max of 5 letters for now. More than that and + # text is too tiny. + letters = self.get_avatar_letters()[0:5] + return { + "letters": letters.lower(), + # Font size is proportional to the number of letters + "font_size": 55 - (max(0, len(letters)) * 5), + "background_color": self.get_avatar_color(), + } + + +class AvatarImageProjectView(AvatarImageBaseView): + + """ + Project avatar configuration. + + Use the project name or slug for the image letters, and use the project pk + to seed the randomization for background color. A remote URL can be + specified by the attached remote repository, if any. + """ + + def get_queryset(self): + return Project.objects.public(self.request.user) + + def get_object(self): + queryset = self.get_queryset() + project_slug = self.kwargs.get("project_slug") + return get_object_or_404( + queryset, + slug=project_slug, + ) + + def get_remote_avatar_url(self): + try: + return self.object.remote_repository.avatar_url + except (AttributeError, ValueError): + return + + def get_avatar_letters(self): + # Try using the project name first, as the slug could have an organization slug + # prepended to the project slug (this leads to redundant acronyms). + words = self.object.name.split(" ") + # However, some project names are just something machine readable + # anyways. See if the slug produces a longer acronym + slug_words = self.object.slug.split("-") + if len(slug_words) > len(words): + words = slug_words + + return "".join([word[0:1] for word in words])