diff --git a/src/dw_design_system/dwds/components/cta.html b/src/dw_design_system/dwds/components/cta.html index e10d6529c..e720d066f 100644 --- a/src/dw_design_system/dwds/components/cta.html +++ b/src/dw_design_system/dwds/components/cta.html @@ -4,4 +4,9 @@ {% if description %}

{{ description }}

{% endif %} {{ extra_content }} + {% if footer_text %} + + {% endif %} diff --git a/src/peoplefinder/services/person.py b/src/peoplefinder/services/person.py index 92457e7e7..31f66526b 100644 --- a/src/peoplefinder/services/person.py +++ b/src/peoplefinder/services/person.py @@ -26,6 +26,7 @@ AuditLogService, ObjectRepr, ) +from peoplefinder.services.team import TeamService from peoplefinder.tasks import notify_user_about_profile_changes, person_update_notifier from peoplefinder.types import EditSections, ProfileSections from user.models import User @@ -299,6 +300,9 @@ def profile_updated( # Notify external services person_update_notifier.delay(person.id) + for team_id in person.roles.all().values_list("team__pk", flat=True).distinct(): + TeamService().clear_profile_completion_cache(team_id) + def profile_deletion_initiated( self, request: Optional[HttpRequest], person: Person, initiated_by: User ) -> None: diff --git a/src/peoplefinder/services/team.py b/src/peoplefinder/services/team.py index 3982e0728..64d030870 100644 --- a/src/peoplefinder/services/team.py +++ b/src/peoplefinder/services/team.py @@ -1,6 +1,7 @@ from typing import Iterator, Optional, TypedDict from django.contrib.postgres.aggregates import ArrayAgg +from django.core.cache import cache from django.db import connection, transaction from django.db.models import ( Case, @@ -16,7 +17,7 @@ from django.db.models.functions import Concat from django.utils.text import slugify -from peoplefinder.models import AuditLog, Team, TeamMember, TeamTree +from peoplefinder.models import AuditLog, Person, Team, TeamMember, TeamTree from peoplefinder.services.audit_log import ( AuditLogSerializer, AuditLogService, @@ -111,7 +112,7 @@ def update_team_parent(self, team: Team, parent: Team) -> None: [team.id, parent.id], ) - def get_all_child_teams(self, parent: Team) -> QuerySet: + def get_all_child_teams(self, parent: Team) -> QuerySet[Team]: """Return all child teams of the given parent team. Args: @@ -261,14 +262,10 @@ def can_team_be_deleted(self, team: Team) -> tuple[bool, list[str]]: reasons = [] sub_teams = self.get_all_child_teams(team) - if sub_teams: reasons.append("sub-teams") - has_members = TeamMember.active.filter( - Q(team=team) | Q(team__in=sub_teams) - ).exists() - + has_members = self.get_team_members(team).exists() if has_members: reasons.append("members") @@ -310,6 +307,48 @@ def team_deleted(self, team: Team, deleted_by: User) -> None: """ AuditLogService.log(AuditLog.Action.DELETE, deleted_by, team) + def get_team_members(self, team: Team) -> QuerySet[TeamMember]: + sub_teams = self.get_all_child_teams(team) + + return TeamMember.active.filter(Q(team=team) | Q(team__in=sub_teams)) + + def get_profile_completion_cache_key(self, team_pk: int) -> str: + return f"team_{team_pk}__profile_completion" + + def clear_profile_completion_cache(self, team_pk: int): + cache.delete(self.get_profile_completion_cache_key(team_pk)) + + def profile_completion(self, team: Team) -> float | None: + """ + Calculate the percentage of users in the team with 100% profile + completion. + + Returns: + float: A percentage + """ + cache_key = self.get_profile_completion_cache_key(team.pk) + if cached_value := cache.get(cache_key, None): + return cached_value + + # Get all people from all teams + people = Person.objects.filter( + id__in=Subquery(self.get_team_members(team).values("person_id")) + ) + completed_profiles = people.filter(profile_completion__gte=100) + + total_members = len(people) + total_completed_profiles = len(completed_profiles) + + if total_members == 0: + return None + + completed_profile_percent = total_completed_profiles / total_members + + # Cache the result for an hour. + timeout = 60 * 60 + cache.set(cache_key, completed_profile_percent, timeout) + return completed_profile_percent + class TeamAuditLogSerializer(AuditLogSerializer): model = Team diff --git a/src/peoplefinder/templates/peoplefinder/components/team-card-new.html b/src/peoplefinder/templates/peoplefinder/components/team-card-new.html index cfdea91d9..529de27d5 100644 --- a/src/peoplefinder/templates/peoplefinder/components/team-card-new.html +++ b/src/peoplefinder/templates/peoplefinder/components/team-card-new.html @@ -2,4 +2,4 @@ {% url 'team-view' team.slug as team_url %} -{% include "dwds/components/cta_card.html" with title=team.name url=team_url extra_content=team.description|markdown|truncatewords_html:25 %} +{% include "dwds/components/cta_card.html" with title=team.name url=team_url extra_content=team.description|markdown|truncatewords_html:25 footer_text=profile_completion %} diff --git a/src/peoplefinder/templates/peoplefinder/team.html b/src/peoplefinder/templates/peoplefinder/team.html index 7f9575872..3cc11256d 100644 --- a/src/peoplefinder/templates/peoplefinder/team.html +++ b/src/peoplefinder/templates/peoplefinder/team.html @@ -23,7 +23,8 @@

{% if team.abbreviation %}({{ team.abbreviation }}){% endif %}

-
{{ team.description|markdown }}
+ {% if profile_completion %}

{{ profile_completion }}

{% endif %} +
{{ team.description|markdown }}
{% if perms.peoplefinder.change_team %} Team leader{{ leaders|length|pluralize }}

Teams within {{ team.short_name }}

{% for team in sub_teams %} -
{% include 'peoplefinder/components/team-card-new.html' with team=team %}
+
+ {% include 'peoplefinder/components/team-card-new.html' with team=team profile_completion=team.profile_completion %} +
{% endfor %}
{% endif %} diff --git a/src/peoplefinder/views/team.py b/src/peoplefinder/views/team.py index d63ac3d5d..820b0f2cc 100644 --- a/src/peoplefinder/views/team.py +++ b/src/peoplefinder/views/team.py @@ -1,7 +1,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import SuspiciousOperation from django.db import models, transaction -from django.db.models import Avg, QuerySet +from django.db.models import QuerySet from django.http import HttpRequest from django.http.response import HttpResponse as HttpResponse from django.shortcuts import redirect @@ -12,7 +12,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView from peoplefinder.forms.team import TeamForm -from peoplefinder.models import Person, Team, TeamMember +from peoplefinder.models import Team, TeamMember from peoplefinder.services.audit_log import AuditLogService from peoplefinder.services.team import TeamService @@ -54,8 +54,19 @@ def parent_teams(self) -> QuerySet[Team]: return TeamService().get_all_parent_teams(self.object) @cached_property - def sub_teams(self) -> QuerySet[Team]: - return TeamService().get_immediate_child_teams(self.object) + def sub_teams(self) -> list[Team]: + sub_teams = [] + + team_service = TeamService() + for sub_team in team_service.get_immediate_child_teams(self.object): + profile_completion = team_service.profile_completion(sub_team) + if profile_completion: + sub_team.profile_completion = ( + f"{profile_completion:.1%} of profiles complete" + ) + sub_teams.append(sub_team) + + return sub_teams @cached_property def leaders(self) -> list[TeamMember]: @@ -96,23 +107,17 @@ def get_context_data(self, **kwargs: dict) -> dict: team_service = TeamService() + if profile_completion := team_service.profile_completion(self.object): + context["profile_completion"] = ( + f"{profile_completion:.1%} of profiles complete" + ) + if self.request.user.has_perm("peoplefinder.delete_team"): ( context["can_team_be_deleted"], context["reasons_team_cannot_be_deleted"], ) = team_service.can_team_be_deleted(self.object) - if self.sub_teams: - # Warning: Multiple requests per sub-team. This might need optimising in the - # future. - for sub_team in self.sub_teams: - sub_team.avg_profile_completion = Person.active.filter( - teams__in=[ - sub_team, - *team_service.get_all_child_teams(sub_team), - ] - ).aggregate(Avg("profile_completion"))["profile_completion__avg"] - if self.request.user.has_perms( ["peoplefinder.change_team", "peoplefinder.view_auditlog"] ):