From a31970c9e199bf9c44ab1a5efc5a65c72f1b30b4 Mon Sep 17 00:00:00 2001 From: Chrissy Wainwright Date: Mon, 24 Mar 2025 16:40:57 -0500 Subject: [PATCH] display user icons on the homepage library spotlight refs #1658 Re-use the code written for the library detail page for displaying authors and maintainers on the homepage. To avoid duplicating code, moved all the necessary pieces to a mixin to be used by HomepageView and LibraryDetail, and adjusted it to work for both. --- ak/tests/test_default_pages.py | 2 +- ak/views.py | 24 +---- libraries/mixins.py | 180 ++++++++++++++++++++++++++++++++- libraries/views.py | 135 +------------------------ templates/homepage.html | 25 ++--- 5 files changed, 196 insertions(+), 170 deletions(-) diff --git a/ak/tests/test_default_pages.py b/ak/tests/test_default_pages.py index 86b85b2a..85dc4ebd 100644 --- a/ak/tests/test_default_pages.py +++ b/ak/tests/test_default_pages.py @@ -4,7 +4,7 @@ import pytest from django.test.utils import override_settings -def test_homepage(library, version, tp): +def test_homepage(library, library_version, version, tp): """Ensure we can hit the homepage""" # Use any page that is named 'home' otherwise use / url = tp.reverse("home") diff --git a/ak/views.py b/ak/views.py index 91bce07f..61f26f66 100644 --- a/ak/views.py +++ b/ak/views.py @@ -9,18 +9,16 @@ from django.views.generic import TemplateView from core.calendar import extract_calendar_events, events_by_month, get_calendar from libraries.constants import LATEST_RELEASE_URL_PATH_STR -from libraries.models import Library +from libraries.mixins import ContributorMixin from news.models import Entry -from versions.models import Version logger = structlog.get_logger() -class HomepageView(TemplateView): +class HomepageView(ContributorMixin, TemplateView): """ - Our default homepage for temp-site. We expect you to not use this view - after you start working on your project. + Define all the pieces that will be displayed on the home page """ template_name = "homepage.html" @@ -28,9 +26,6 @@ class HomepageView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["entries"] = Entry.objects.published().order_by("-publish_at")[:3] - latest_version = Version.objects.most_recent() - context["latest_version"] = latest_version - context["featured_library"] = self.get_featured_library(latest_version) context["events"] = self.get_events() if context["events"]: context["num_months"] = len(context["events"]) @@ -64,19 +59,6 @@ class HomepageView(TemplateView): return dict(sorted_events) - def get_featured_library(self, latest_version): - library = Library.objects.filter(featured=True).first() - - # If we don't have a featured library, return a random library - if not library: - library = ( - Library.objects.filter(library_version__version=latest_version) - .order_by("?") - .first() - ) - - return library - class ForbiddenView(View): """ diff --git a/libraries/mixins.py b/libraries/mixins.py index accf5b31..a5f0b26d 100644 --- a/libraries/mixins.py +++ b/libraries/mixins.py @@ -1,4 +1,8 @@ import structlog +from types import SimpleNamespace + +from django.db.models import Count, Exists, OuterRef +from django.db.models.functions import Lower from django.shortcuts import get_object_or_404 from django.urls import reverse @@ -7,7 +11,13 @@ from libraries.constants import ( MASTER_RELEASE_URL_PATH_STR, DEVELOP_RELEASE_URL_PATH_STR, ) -from libraries.models import Library +from libraries.models import ( + Commit, + CommitAuthor, + CommitAuthorEmail, + Library, + LibraryVersion, +) from versions.models import Version logger = structlog.get_logger() @@ -70,3 +80,171 @@ class BoostVersionMixin: ) # here we hack extra_context into the request so we can access for cookie checks request.extra_context = self.extra_context + + +class ContributorMixin: + """Mixin to gather a list of all authors, maintainers, and + contributors without duplicates. + Uses the current Library if on the Library detail view, + otherwise grabs a featured library + """ + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["latest_version"] = Version.objects.most_recent() + + if hasattr(self, "object") and isinstance(self.object, Library): + library = self.object + try: + library_version = LibraryVersion.objects.get( + library=library, version=context["selected_version"] + ) + except LibraryVersion.DoesNotExist: + return context + else: + library_version = self.get_featured_library() + context["featured_library"] = library_version + + context["authors"] = self.get_related(library_version, "authors") + context["maintainers"] = self.get_related( + library_version, + "maintainers", + exclude_ids=[x.id for x in context["authors"]], + ) + context["author_tag"] = self.get_author_tag(library_version) + exclude_maintainer_ids = [ + x.commitauthor.id + for x in context["maintainers"] + if getattr(x.commitauthor, "id", None) + ] + exclude_author_ids = [ + x.commitauthor.id + for x in context["authors"] + if getattr(x.commitauthor, "id", None) + ] + top_contributors_release = self.get_top_contributors( + library_version=library_version, + exclude=exclude_maintainer_ids + exclude_author_ids, + ) + context["top_contributors_release_new"] = [ + x for x in top_contributors_release if x.is_new + ] + context["top_contributors_release_old"] = [ + x for x in top_contributors_release if not x.is_new + ] + exclude_top_contributor_ids = [x.id for x in top_contributors_release] + context["previous_contributors"] = self.get_previous_contributors( + library_version, + exclude=exclude_maintainer_ids + + exclude_top_contributor_ids + + exclude_author_ids, + ) + return context + + def get_featured_library(self): + """Returns latest LibraryVersion associated with the featured Library""" + # If multiple are featured, pick one at random + latest_version = Version.objects.most_recent() + library = Library.objects.filter(featured=True).order_by("?").first() + + # If we don't have a featured library, return a random library + if not library: + library = ( + Library.objects.filter(library_version__version=latest_version) + .order_by("?") + .first() + ) + if not library: + return None + libversion = LibraryVersion.objects.filter( + library_id=library.id, version=latest_version + ).first() + + return libversion + + def get_related(self, library_version, relation="maintainers", exclude_ids=None): + """Get the maintainers|authors for the current LibraryVersion. + + Also patches the CommitAuthor onto the user, if a matching email exists. + """ + if relation == "maintainers": + qs = library_version.maintainers.all() + elif relation == "authors": + qs = library_version.authors.all() + else: + raise ValueError("relation must be maintainers or authors.") + if exclude_ids: + qs = qs.exclude(id__in=exclude_ids) + qs = list(qs) + commit_authors = { + author_email.email: author_email + for author_email in CommitAuthorEmail.objects.annotate( + email_lower=Lower("email") + ) + .filter(email_lower__in=[x.email.lower() for x in qs]) + .select_related("author") + } + for user in qs: + if author_email := commit_authors.get(user.email.lower(), None): + user.commitauthor = author_email.author + else: + user.commitauthor = SimpleNamespace( + github_profile_url="", + avatar_url="", + display_name=f"{user.display_name}", + ) + return qs + + def get_author_tag(self, library_version): + """Format the authors for the author meta tag in the template.""" + author_names = list( + library_version.library.authors.values_list("display_name", flat=True) + ) + if len(author_names) > 1: + final_output = ", ".join(author_names[:-1]) + " and " + author_names[-1] + else: + final_output = author_names[0] if author_names else "" + + return final_output + + def get_top_contributors(self, library_version=None, exclude=None): + if library_version: + prev_versions = Version.objects.minor_versions().filter( + version_array__lt=library_version.version.cleaned_version_parts_int + ) + qs = CommitAuthor.objects.filter( + commit__library_version=library_version + ).annotate( + is_new=~Exists( + Commit.objects.filter( + author_id=OuterRef("id"), + library_version__in=LibraryVersion.objects.filter( + version__in=prev_versions, library=library_version.library + ), + ) + ) + ) + else: + qs = CommitAuthor.objects.filter( + commit__library_version__library=self.object + ) + if exclude: + qs = qs.exclude(id__in=exclude) + qs = qs.annotate(count=Count("commit")).order_by("-count") + return qs + + def get_previous_contributors(self, library_version, exclude=None): + library_versions = LibraryVersion.objects.filter( + library=library_version.library, + version__in=Version.objects.minor_versions().filter( + version_array__lt=library_version.version.cleaned_version_parts_int + ), + ) + qs = ( + CommitAuthor.objects.filter(commit__library_version__in=library_versions) + .annotate(count=Count("commit")) + .order_by("-count") + ) + if exclude: + qs = qs.exclude(id__in=exclude) + return qs diff --git a/libraries/views.py b/libraries/views.py index e49513e1..4a2751ed 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -1,9 +1,7 @@ import datetime -from types import SimpleNamespace from django.contrib import messages -from django.db.models import F, Count, Exists, OuterRef, Prefetch -from django.db.models.functions import Lower +from django.db.models import F, Count, Prefetch from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator @@ -15,12 +13,9 @@ from core.githubhelper import GithubAPIClient from versions.models import Version from .constants import README_MISSING -from .mixins import VersionAlertMixin, BoostVersionMixin +from .mixins import VersionAlertMixin, BoostVersionMixin, ContributorMixin from .models import ( Category, - Commit, - CommitAuthor, - CommitAuthorEmail, Library, LibraryVersion, ) @@ -208,7 +203,7 @@ class LibraryCategorized(LibraryListBase): @method_decorator(csrf_exempt, name="dispatch") -class LibraryDetail(VersionAlertMixin, BoostVersionMixin, DetailView): +class LibraryDetail(VersionAlertMixin, BoostVersionMixin, ContributorMixin, DetailView): """Display a single Library in insolation""" model = Library @@ -240,40 +235,7 @@ class LibraryDetail(VersionAlertMixin, BoostVersionMixin, DetailView): if library_version else self.object.github_url ) - context["authors"] = self.get_related(library_version, "authors") - context["maintainers"] = self.get_related( - library_version, - "maintainers", - exclude_ids=[x.id for x in context["authors"]], - ) - context["author_tag"] = self.get_author_tag() - exclude_maintainer_ids = [ - x.commitauthor.id - for x in context["maintainers"] - if getattr(x.commitauthor, "id", None) - ] - exclude_author_ids = [ - x.commitauthor.id - for x in context["authors"] - if getattr(x.commitauthor, "id", None) - ] - top_contributors_release = self.get_top_contributors( - version=context["selected_version"], - exclude=exclude_maintainer_ids + exclude_author_ids, - ) - context["top_contributors_release_new"] = [ - x for x in top_contributors_release if x.is_new - ] - context["top_contributors_release_old"] = [ - x for x in top_contributors_release if not x.is_new - ] - exclude_top_contributor_ids = [x.id for x in top_contributors_release] - context["previous_contributors"] = self.get_previous_contributors( - context["selected_version"], - exclude=exclude_maintainer_ids - + exclude_top_contributor_ids - + exclude_author_ids, - ) + # Populate the commit graphs context["commit_data_by_release"] = self.get_commit_data_by_release() context["dependency_diff"] = self.get_dependency_diff(library_version) @@ -309,17 +271,6 @@ class LibraryDetail(VersionAlertMixin, BoostVersionMixin, DetailView): for x in reversed(list(qs)) ] - def get_author_tag(self): - """Format the authors for the author meta tag in the template.""" - authors = self.object.authors.all() - author_names = [author.display_name for author in authors] - if len(author_names) > 1: - final_output = ", ".join(author_names[:-1]) + " and " + author_names[-1] - else: - final_output = author_names[0] if author_names else "" - - return final_output - def _prepare_commit_data(self, commit_data, data_type): commit_data_list = [] for data in commit_data: @@ -345,84 +296,6 @@ class LibraryDetail(VersionAlertMixin, BoostVersionMixin, DetailView): # This should never happen because it should be caught in get_object return self.object.github_url - def get_related(self, library_version, relation="maintainers", exclude_ids=None): - """Get the maintainers|authors for the current LibraryVersion. - - Also patches the CommitAuthor onto the user, if a matching email exists. - """ - if relation == "maintainers": - qs = library_version.maintainers.all() - elif relation == "authors": - qs = library_version.authors.all() - else: - raise ValueError("relation must be maintainers or authors.") - if exclude_ids: - qs = qs.exclude(id__in=exclude_ids) - qs = list(qs) - commit_authors = { - author_email.email: author_email - for author_email in CommitAuthorEmail.objects.annotate( - email_lower=Lower("email") - ) - .filter(email_lower__in=[x.email.lower() for x in qs]) - .select_related("author") - } - for user in qs: - if author_email := commit_authors.get(user.email.lower(), None): - user.commitauthor = author_email.author - else: - user.commitauthor = SimpleNamespace( - github_profile_url="", - avatar_url="", - display_name=f"{user.display_name}", - ) - return qs - - def get_top_contributors(self, version=None, exclude=None): - if version: - library_version = LibraryVersion.objects.get( - library=self.object, version=version - ) - prev_versions = Version.objects.minor_versions().filter( - version_array__lt=version.cleaned_version_parts_int - ) - qs = CommitAuthor.objects.filter( - commit__library_version=library_version - ).annotate( - is_new=~Exists( - Commit.objects.filter( - author_id=OuterRef("id"), - library_version__in=LibraryVersion.objects.filter( - version__in=prev_versions, library=self.object - ), - ) - ) - ) - else: - qs = CommitAuthor.objects.filter( - commit__library_version__library=self.object - ) - if exclude: - qs = qs.exclude(id__in=exclude) - qs = qs.annotate(count=Count("commit")).order_by("-count") - return qs - - def get_previous_contributors(self, version, exclude=None): - library_versions = LibraryVersion.objects.filter( - library=self.object, - version__in=Version.objects.minor_versions().filter( - version_array__lt=version.cleaned_version_parts_int - ), - ) - qs = ( - CommitAuthor.objects.filter(commit__library_version__in=library_versions) - .annotate(count=Count("commit")) - .order_by("-count") - ) - if exclude: - qs = qs.exclude(id__in=exclude) - return qs - def get_version(self): """Get the version of Boost for the library we're currently looking at.""" version_slug = self.kwargs.get("version_slug") diff --git a/templates/homepage.html b/templates/homepage.html index a6099dc6..03c353d7 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -133,29 +133,22 @@ All Libraries  -

{{ featured_library.name }}

+

{{ featured_library.library.name }}

{{ featured_library.description }}

{% if featured_library.first_boost_version %}Added in {{ featured_library.first_boost_version.display_name }}{% endif %}

- {% if featured_library.authors %} + {% if authors or maintainers %}
- {% for author in featured_library.authors.all %} -
-
- {% if author.image %} - {{ author.display_name }} - {% else %} - - {% endif %} -
- {{ author.display_name }} +
+ {% for user in authors %} + {% avatar user=user commitauthor=user.commitauthor avatar_type="wide" contributor_label="Author" %} + {% endfor %} + {% for user in maintainers %} + {% avatar user=user commitauthor=user.commitauthor avatar_type="wide" contributor_label="Maintainer" %} + {% endfor %}
- {% endfor %}
{% endif %}