Skip to content

Commit

Permalink
Firebreak: Centralised profiles (#387)
Browse files Browse the repository at this point in the history
Co-authored-by: Sam Dudley <samuel.dudley@digital.trade.gov.uk>
Co-authored-by: Cameron Lamb <cameron.lamb@digital.trade.gov.uk>
  • Loading branch information
3 people authored Sep 12, 2023
1 parent 44a19b3 commit 9654364
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 4 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ extension = "html"
profile = "django"
ignore = "T002,H006,H017,H023"
preserve_blank_lines = true
extend_exclude = "htmlcov,staticfiles,static,node_modules"
3 changes: 3 additions & 0 deletions src/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,11 @@
"person-api-people-list",
"person-api-people-detail",
"team-api-teams-list",
"profile-get-card",
)

AUTHBROKER_INTROSPECTION_TOKEN = env("AUTHBROKER_INTROSPECTION_TOKEN", default="XXX")

# There are some big pages with lots of content that need to send many fields.
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240

Expand Down
1 change: 0 additions & 1 deletion src/peoplefinder/forms/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.contrib.auth import get_user_model
from django.core.validators import ValidationError


User = get_user_model()


Expand Down
19 changes: 19 additions & 0 deletions src/peoplefinder/services/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, TypedDict

import requests
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.postgres.aggregates import ArrayAgg
Expand Down Expand Up @@ -554,6 +555,24 @@ def get_profile_section_values(
)
return values

@staticmethod
def get_verified_emails(person: Person) -> list[str]:
user_email = person.user.email # @TODO prefer UUID if we can get it from SSO
url = f"{settings.AUTHBROKER_URL}/api/v1/user/emails/"
params = {"email": user_email}
headers = {"Authorization": f"bearer {settings.AUTHBROKER_INTROSPECTION_TOKEN}"}

response = requests.get(url, params, headers=headers, timeout=5)

if response.status_code == 200:
resp_json = response.json()
return resp_json["emails"]
else:
logger.error(
f"Response code [{response.status_code}] from authbroker emails endpoint for {user_email}"
)
return []


class PersonAuditLogSerializer(AuditLogSerializer):
model = Person
Expand Down
138 changes: 138 additions & 0 deletions src/peoplefinder/static/peoplefinder/profile-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
(function () {
/**
*
* @param {String} html
* @returns {Node}
*/
function htmlTemplate(html) {
const template = document.createElement("template");
template.innerHTML = html;

return template.content.cloneNode(true);
}

/**
*
* @returns {String}
*/
function getProfileCardUrl() {
const metaName = "profile-card-url";
const meta = document.querySelector(`meta[name="${metaName}"]`);

return meta.content;
}

/**
*
* @returns {Node}
*/
async function getProfileCard() {
const url = getProfileCardUrl();

const response = await fetch(url, {
credentials: "include",
cache: "default",
});

if (!response.ok) {
throw new Error("Error fetching profile card");
}

const html = await response.text();

return htmlTemplate(html);
}

/**
*
*/
class ProfileCard extends HTMLElement {
constructor() {
super();

const css = `
<style>
:host {
display: inline-block;
width: 100%;
}
a {
color: inherit;
text-decoration: none;
}
ul {
padding: 0;
list-style-type: none;
}
.profile-card {
font-family: "GDS Transport", arial, sans-serif;
height: 40px;
display: flex;
flex-direction: row-reverse;
gap: 0.5rem;
align-items: center;
container-type: inline-block;
}
.profile-photo {
height: 100%;
object-fit: cover;
aspect-ratio: 1 / 1;
}
.profile-details {
text-align: right;
}
.profile-name {
text-decoration: underline;
}
.profile-completion {
font-size: 0.875rem;
}
.loading .profile-photo {
background-color: lightgray;
}
</style>
<div class="profile-card loading">
<div class="profile-photo"></div>
<div class="profile-details">
<ul>
<li class="profile-name"></li>
<li>Loading...</li>
</ul>
</div>
</div>
`;

const cssTemplate = htmlTemplate(css);

this.attachShadow({ mode: "open" });
this.shadowRoot.append(cssTemplate);
}

async connectedCallback() {
const profileName = this.getAttribute("profile-name");

if (profileName) {
this.find(".profile-name").innerHTML = profileName;
}

let html = null;

try {
html = await getProfileCard();
} catch (error) {
console.log(error);
}

if (html) {
this.shadowRoot.querySelector(".loading").style.display = "none";
this.shadowRoot.append(html);
}
}

find(selector) {
return this.shadowRoot.querySelector(selector);
}
}

customElements.define("profile-card", ProfileCard);
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% comment %}
context:
profile (Person)
profile_url (str)
no_photo_url (str)
{% endcomment %}
<a href="{{ profile_url }}" target="_blank">
<div class="profile-card" data-profile-uuid="{{ profile.slug }}">
{% if profile and profile.photo_small %}
<img class="profile-photo"
src="{{ profile.photo_small.url }}"
alt="Photo of {{ profile.full_name }}">
{% else %}
<img class="profile-photo" src="{{ no_photo_url }}" alt="No photo">
{% endif %}
<ul class="profile-details">
{% if profile %}
<li class="profile-name">Hi {{ profile.preferred_first_name }}</li>
<li class="profile-completion">
View your profile
{% if profile.profile_completion < 100 %}
({{ profile.profile_completion }}% complete)
{% endif %}
</li>
{% else %}
<li class="profile-not-found">No profile found</li>
{% endif %}
</ul>
</div>
</div>
</a>
1 change: 0 additions & 1 deletion src/peoplefinder/templates/peoplefinder/profile-edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ <h1 class="govuk-heading-l">{{ page_title }}</h1>
<nav class="moj-side-navigation govuk-!-padding-top-0"
aria-label="Side navigation">
<ul class="moj-side-navigation__list">

{% for edit_section in edit_sections %}
<li class="moj-side-navigation__item {% if current_edit_section == edit_section %}moj-side-navigation__item--active{% endif %}">
<a href="{% url 'profile-edit-section' profile_slug=profile_slug edit_section=edit_section.value %}"
Expand Down
2 changes: 1 addition & 1 deletion src/peoplefinder/templatetags/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def byte_hash(val: str):
"style": (
f"--color-1: {byte_hash(profile.first_name)};"
f" --color-2: {byte_hash(profile.last_name)};"
f" --color-3: {byte_hash(profile.first_name + profile.last_name)}"
f" --color-3: {byte_hash(profile.preferred_first_name)}"
),
}
return mark_safe(" ".join([f'{k}="{v}"' for k, v in attrs.items()])) # noqa S308
6 changes: 6 additions & 0 deletions src/peoplefinder/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ProfileLegacyView,
ProfileUpdateUserView,
get_profile_by_staff_sso_id,
get_profile_card,
profile_edit_blank_teams_form,
redirect_to_profile_edit,
)
Expand Down Expand Up @@ -123,6 +124,11 @@
ProfileUpdateUserView.as_view(),
name="profile-update-user",
),
path(
"<str:staff_sso_email_user_id>/card",
get_profile_card,
name="profile-get-card",
),
path(
"get-by-staff-sso-id/<str:staff_sso_id>/",
get_profile_by_staff_sso_id,
Expand Down
29 changes: 28 additions & 1 deletion src/peoplefinder/views/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
HttpResponseRedirect,
)
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.template.response import TemplateResponse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.decorators import decorator_from_middleware, method_decorator
from django.views.generic import TemplateView
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import UpdateView
from django_hawk.middleware import HawkResponseMiddleware
from django_hawk.utils import DjangoHawkAuthenticationFailed, authenticate_request
from webpack_loader.utils import get_static

from peoplefinder.forms.crispy_helper import RoleFormsetFormHelper
from peoplefinder.forms.profile import ProfileUpdateUserForm
Expand Down Expand Up @@ -512,3 +516,26 @@ def get_profile_by_staff_sso_id(request, staff_sso_id):
person = get_object_or_404(Person, user__legacy_sso_user_id=staff_sso_id)

return redirect(person)


@decorator_from_middleware(HawkResponseMiddleware)
def get_profile_card(request, staff_sso_email_user_id):
try:
authenticate_request(request=request)
except DjangoHawkAuthenticationFailed:
return HttpResponse(status=401)

try:
person = Person.objects.filter(user__username=staff_sso_email_user_id).get()
except (Person.DoesNotExist, Person.MultipleObjectsReturned):
person = None

return TemplateResponse(
request,
"peoplefinder/components/profile-card.html",
{
"profile": person,
"profile_url": request.build_absolute_uri(person.get_absolute_url()),
"no_photo_url": request.build_absolute_uri(get_static("no-photo.png")),
},
)

0 comments on commit 9654364

Please sign in to comment.