Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Firebreak: Centralised profiles #387

Merged
merged 34 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
49733c0
add verified email validation
marcelkornblum Jul 8, 2023
fe58b47
attempt to use select widget
marcelkornblum Jul 8, 2023
31f27ec
better default
marcelkornblum Jul 10, 2023
af83e28
lint
marcelkornblum Jul 10, 2023
1781f65
Add profile card API and web component
SamDudley Jul 10, 2023
82d4e96
Merge branch 'mk-firebreak' into firebreak
marcelkornblum Jul 11, 2023
9dd0d35
rollback select
marcelkornblum Jul 11, 2023
7c1f39e
Add profile card API and web component
SamDudley Jul 10, 2023
859c5d5
merge migrations
SamDudley Jul 11, 2023
0d1b98b
fix djlint config
SamDudley Jul 11, 2023
bbbe8a7
Make migrations linear
CamLamb Jul 11, 2023
f9bc4c7
fix meta name
SamDudley Jul 11, 2023
86c6ca5
more fixes for the web component
SamDudley Jul 11, 2023
9f0ba40
missing /
marcelkornblum Jul 11, 2023
011f175
try changing email to select
marcelkornblum Jul 11, 2023
d86b865
try again with choices
marcelkornblum Jul 11, 2023
730cd45
too many brackets :(
marcelkornblum Jul 11, 2023
da5027c
stupid brackets
marcelkornblum Jul 11, 2023
0532c53
dont use tuples
marcelkornblum Jul 11, 2023
554ad07
comment out select
marcelkornblum Jul 11, 2023
0fc32e5
attempt
marcelkornblum Jul 11, 2023
f11a400
revert
marcelkornblum Jul 11, 2023
be8ee8c
Merge branch 'main'
CamLamb Sep 11, 2023
aa3168e
Run `make fix`
CamLamb Sep 11, 2023
2fb7df0
Add email choices to the new forms
CamLamb Sep 11, 2023
90c16fc
Update migrations
CamLamb Sep 11, 2023
ab46ad2
Update the profile-card
CamLamb Sep 11, 2023
edd6e52
Merge branch 'main' into firebreak
CamLamb Sep 12, 2023
41d92de
Fix the email select field
CamLamb Sep 12, 2023
0858397
Revert the form field to being read only
CamLamb Sep 12, 2023
d106a17
Remove clean and get choices methods
CamLamb Sep 12, 2023
abf834c
Revert the help text changes
CamLamb Sep 12, 2023
e3ebab5
Remove unused imports
CamLamb Sep 12, 2023
a6c3957
Remove typo
CamLamb Sep 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
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")),
},
)