Skip to content

Commit

Permalink
Add profile completion to teams (#829)
Browse files Browse the repository at this point in the history
  • Loading branch information
CamLamb authored Dec 9, 2024
1 parent f4d2e8a commit 4cc8267
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 25 deletions.
5 changes: 5 additions & 0 deletions src/dw_design_system/dwds/components/cta.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
{% if description %}<p>{{ description }}</p>{% endif %}
{{ extra_content }}
</div>
{% if footer_text %}
<div class="content-footer content-stack">
<p>{{ footer_text }}</p>
</div>
{% endif %}
</div>
4 changes: 4 additions & 0 deletions src/peoplefinder/services/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
53 changes: 46 additions & 7 deletions src/peoplefinder/services/team.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
7 changes: 5 additions & 2 deletions src/peoplefinder/templates/peoplefinder/team.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ <h1>
{% if team.abbreviation %}({{ team.abbreviation }}){% endif %}
</h1>

<div class="text-small" data-testid="description">{{ team.description|markdown }}</div>
{% if profile_completion %}<p class="text-small">{{ profile_completion }}</p>{% endif %}
<div data-testid="description">{{ team.description|markdown }}</div>
<div class="dwds-button-group">
{% if perms.peoplefinder.change_team %}
<a class="dwds-button dwds-button--secondary dwds-button--inline"
Expand Down Expand Up @@ -77,7 +78,9 @@ <h2>Team leader{{ leaders|length|pluralize }}</h2>
<h2>Teams within {{ team.short_name }}</h2>
<div class="content-grid grid-cards-auto-fill">
{% for team in sub_teams %}
<div class="dwds-content-item-card">{% include 'peoplefinder/components/team-card-new.html' with team=team %}</div>
<div class="dwds-content-item-card">
{% include 'peoplefinder/components/team-card-new.html' with team=team profile_completion=team.profile_completion %}
</div>
{% endfor %}
</div>
{% endif %}
Expand Down
35 changes: 20 additions & 15 deletions src/peoplefinder/views/team.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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"]
):
Expand Down

0 comments on commit 4cc8267

Please sign in to comment.