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.
This commit is contained in:
Chrissy Wainwright
2025-03-24 16:40:57 -05:00
parent 4860978c56
commit a31970c9e1
5 changed files with 196 additions and 170 deletions

View File

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

View File

@@ -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):
"""

View File

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

View File

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

View File

@@ -133,29 +133,22 @@
All Libraries&nbsp;<i class="fas fa-chevron-right text-sky-600 dark:text-sky-300 group-hover:text-orange dark:group-hover:text-orange"></i>
</a>
</div>
<h3 class="pb-2 mb-4 text-lg md:text-2xl capitalize border-b border-gray-400 text-orange dark:border-slate"><a href="{% url 'library-detail' library_slug=featured_library.slug version_slug=LATEST_RELEASE_URL_PATH_STR %}" class="link-header">{{ featured_library.name }}</a></h3>
<h3 class="pb-2 mb-4 text-lg md:text-2xl capitalize border-b border-gray-400 text-orange dark:border-slate"><a href="{% url 'library-detail' library_slug=featured_library.library.slug version_slug=LATEST_RELEASE_URL_PATH_STR %}" class="link-header">{{ featured_library.library.name }}</a></h3>
<span class="pb-1 mx-auto w-full text-sm md:text-base align-left">
{{ featured_library.description }}
</span>
<p class="pb-1 mx-auto w-full text-xs md:text-sm align-left">{% if featured_library.first_boost_version %}Added in {{ featured_library.first_boost_version.display_name }}{% endif %}</p>
<div class="py-4">
{% if featured_library.authors %}
{% if authors or maintainers %}
<div class="flex flex-wrap justify-start">
{% for author in featured_library.authors.all %}
<div class="p-1 md:p-2 w-min text-center flex flex-col items-center justify-center">
<div class="bg-gray-300 dark:bg-slate rounded-lg w-[36px] h-[36px]">
{% if author.image %}
<img src="{{ author.image.url }}"
title="{{ author.display_name }}"
alt="{{ author.display_name }}"
class="rounded-lg w-[36px] h-[36px]" />
{% else %}
<i class="text-3xl fas fa-user text-white dark:text-white/60" title="{{ author.display_name }}"></i>
{% endif %}
</div>
<span class="text-xs">{{ author.display_name }}</span>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-3 gap-x-2">
{% 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 %}
</div>
</div>
{% endif %}