Skip to content

Commit da3486c

Browse files
authored
Merge pull request #167 from NHSDigital/dtoss-9247-screening-history-section
[DTOSS-9247] Participant record appointments section
2 parents ee1be7b + e980fcb commit da3486c

10 files changed

Lines changed: 371 additions & 41 deletions

File tree

manage_breast_screening/mammograms/presenters.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,7 @@
44

55
from ..core.utils.date_formatting import format_date, format_relative_date, format_time
66
from ..participants.models import AppointmentStatus
7-
from ..participants.presenters import ParticipantPresenter
8-
9-
10-
def status_colour(status):
11-
"""
12-
Color to render the status tag
13-
"""
14-
match status:
15-
case AppointmentStatus.CHECKED_IN:
16-
return "" # no colour will get solid dark blue
17-
case AppointmentStatus.SCREENED:
18-
return "green"
19-
case AppointmentStatus.DID_NOT_ATTEND | AppointmentStatus.CANCELLED:
20-
return "red"
21-
case (
22-
AppointmentStatus.ATTENDED_NOT_SCREENED
23-
| AppointmentStatus.PARTIALLY_SCREENED
24-
):
25-
return "orange"
26-
case _:
27-
return "blue" # default blue
7+
from ..participants.presenters import ParticipantPresenter, status_colour
288

299

3010
def present_secondary_nav(id):

manage_breast_screening/participants/fixtures/participants.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@
8686
"date_of_birth": "1959-07-22",
8787
"ethnic_group": null,
8888
"risk_level": "Routine",
89-
"extra_needs": [
90-
"Wheelchair user"
91-
]
89+
"extra_needs": ["Wheelchair user"]
9290
}
9391
},
9492
{

manage_breast_screening/participants/jinja2/participants/show.jinja

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{% extends 'layout-app.jinja' %}
2-
{% from 'card/macro.jinja' import card %}
32
{% from 'back-link/macro.jinja' import backLink %}
3+
{% from 'card/macro.jinja' import card %}
4+
{% from 'tag/macro.jinja' import tag %}
45
{% from 'components/participant-details/macro.jinja' import participant_details %}
6+
{% from 'summary-list/macro.jinja' import summaryList %}
57

68
{% block beforeContent %}
79
{{ backLink({
@@ -18,14 +20,70 @@
1820
</h1>
1921

2022
{% set personal_details_html %}
21-
{{ participant_details(participant=participant, full_details=true, show_last_known_mammogram=false) }}
23+
{{ participant_details(participant=presented_participant, full_details=true, show_last_known_mammogram=false) }}
2224
{% endset %}
2325

2426
{{ card({
2527
"heading": "Personal details",
2628
"headingLevel": "2",
2729
"descriptionHtml": personal_details_html
2830
}) }}
31+
32+
{% macro appointment_table(presented_appointments, testid) %}
33+
<table class="nhsuk-table" data-testid="{{ testid }}">
34+
<thead>
35+
<tr>
36+
<th>Date</th>
37+
<th>Type</th>
38+
<th>Location</th>
39+
<th>Status</th>
40+
<th>Details</th>
41+
</tr>
42+
</thead>
43+
<tbody>
44+
{% for presented_appointment in presented_appointments %}
45+
<tr data-testid="appointment-row">
46+
<td>{{ presented_appointment.starts_at }}</td>
47+
<td>{{ presented_appointment.clinic_type }}</td>
48+
<td>{{ presented_appointment.setting_name }}</td>
49+
<td>
50+
{{ tag({
51+
"text": presented_appointment.status.text,
52+
"classes": presented_appointment.status.classes
53+
}) }}
54+
</td>
55+
<td>
56+
<a href="{{ presented_appointment.url }}">
57+
View details
58+
</a>
59+
</td>
60+
</tr>
61+
{% endfor %}
62+
</tbody>
63+
</table>
64+
{% endmacro %}
65+
66+
{% set appointments_html %}
67+
{% if presented_appointments.upcoming %}
68+
<h3>Upcoming</h3>
69+
{{ appointment_table(presented_appointments.upcoming, testid="upcoming-appointments-table") }}
70+
{% endif %}
71+
72+
{% if presented_appointments.past %}
73+
<h3>Previous</h3>
74+
{{ appointment_table(presented_appointments.past, testid="past-appointments-table") }}
75+
{% endif %}
76+
77+
{% if not presented_appointments.past and not presented_appointments.upcoming %}
78+
<p>No screening history found</p>
79+
{% endif %}
80+
{% endset %}
81+
82+
{{ card({
83+
"heading": "Appointments",
84+
"headingLevel": "2",
85+
"descriptionHtml": appointments_html
86+
}) }}
2987
</div>
3088
</div>
3189
{% endblock page_content %}

manage_breast_screening/participants/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ def complete(self):
149149
AppointmentStatus.ATTENDED_NOT_SCREENED,
150150
)
151151

152+
def upcoming(self):
153+
return self.filter(clinic_slot__starts_at__gte=date.today())
154+
155+
def past(self):
156+
return self.filter(clinic_slot__starts_at__lt=date.today())
157+
152158
def for_clinic_and_filter(self, clinic, filter):
153159
match filter:
154160
case "remaining":

manage_breast_screening/participants/presenters.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from dataclasses import dataclass
2+
from typing import Any
3+
14
from django.urls import reverse
25

36
from ..core.utils.date_formatting import format_date, format_relative_date
@@ -7,6 +10,27 @@
710
format_phone_number,
811
sentence_case,
912
)
13+
from .models import AppointmentStatus
14+
15+
16+
def status_colour(status):
17+
"""
18+
Color to render the status tag
19+
"""
20+
match status:
21+
case AppointmentStatus.CHECKED_IN:
22+
return "" # no colour will get solid dark blue
23+
case AppointmentStatus.SCREENED:
24+
return "green"
25+
case AppointmentStatus.DID_NOT_ATTEND | AppointmentStatus.CANCELLED:
26+
return "red"
27+
case (
28+
AppointmentStatus.ATTENDED_NOT_SCREENED
29+
| AppointmentStatus.PARTIALLY_SCREENED
30+
):
31+
return "orange"
32+
case _:
33+
return "blue" # default blue
1034

1135

1236
class ParticipantPresenter:
@@ -63,3 +87,45 @@ def last_known_screening(self):
6387
if self._last_known_screening
6488
else {}
6589
)
90+
91+
92+
class ParticipantAppointmentsPresenter:
93+
@dataclass
94+
class PresentedAppointment:
95+
starts_at: str
96+
clinic_type: str
97+
setting_name: str
98+
status: dict[str, Any]
99+
url: str
100+
101+
def __init__(self, past_appointments, upcoming_appointments):
102+
self.past = [
103+
self._present_appointment(appointment) for appointment in past_appointments
104+
]
105+
self.upcoming = [
106+
self._present_appointment(appointment)
107+
for appointment in upcoming_appointments
108+
]
109+
110+
def _present_appointment(self, appointment):
111+
clinic_slot = appointment.clinic_slot
112+
clinic = clinic_slot.clinic
113+
setting = clinic.setting
114+
115+
return self.PresentedAppointment(
116+
starts_at=format_date(clinic_slot.starts_at),
117+
clinic_type=clinic.get_type_display().capitalize(),
118+
setting_name=sentence_case(setting.name),
119+
status=self._present_status(appointment),
120+
url=reverse("mammograms:start_screening", kwargs={"id": appointment.pk}),
121+
)
122+
123+
def _present_status(self, appointment):
124+
current_status = appointment.current_status
125+
colour = status_colour(current_status.state)
126+
127+
return {
128+
"classes": f"nhsuk-tag--{colour} app-nowrap" if colour else "app-nowrap",
129+
"text": current_status.get_state_display(),
130+
"key": current_status.state,
131+
}

manage_breast_screening/participants/tests/factories.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ class Meta:
5959
clinic_slot = SubFactory(ClinicSlotFactory)
6060
screening_episode = SubFactory(ScreeningEpisodeFactory)
6161

62+
@post_generation
63+
def starts_at(obj, create, extracted, **kwargs):
64+
if not create or not extracted:
65+
return
66+
67+
obj.clinic_slot.starts_at = extracted
68+
if create:
69+
obj.clinic_slot.save()
70+
6271
# Allow passing an explicit status
6372
# e.g. `current_status=AppointmentStatus.CHECKED_IN`
6473
@post_generation

manage_breast_screening/participants/tests/system/test_participant_record.py

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import re
2+
from datetime import datetime
3+
from datetime import timezone as tz
24

35
import pytest
46
from django.urls import reverse
57
from playwright.sync_api import expect
68

9+
from manage_breast_screening.clinics.tests.factories import ClinicSlotFactory
710
from manage_breast_screening.core.system_test_setup import SystemTestCase
811
from manage_breast_screening.participants.tests.factories import (
912
AppointmentFactory,
@@ -14,14 +17,15 @@
1417

1518
class TestParticipantRecord(SystemTestCase):
1619
@pytest.fixture(autouse=True)
17-
def before(self):
20+
def before(self, time_machine):
1821
self.participant = ParticipantFactory(first_name="Janet", last_name="Williams")
19-
self.screening_episode = ScreeningEpisodeFactory(participant=self.participant)
20-
self.appointment = AppointmentFactory(screening_episode=self.screening_episode)
22+
23+
time_machine.move_to(datetime(2025, 1, 1, 10, tzinfo=tz.utc))
2124

2225
@pytest.mark.skip("not implemented yet")
2326
def test_viewing_participant_record_from_an_appointment(self):
24-
self.given_i_am_viewing_an_appointment()
27+
self.given_the_participant_has_an_upcoming_appointment()
28+
self.given_i_am_viewing_the_upcoming_appointment()
2529
self.when_i_click_on_view_participant_record()
2630
self.then_i_should_be_on_the_participant_record_page()
2731
self.and_i_should_see_the_participant_record()
@@ -32,12 +36,53 @@ def test_accessibility(self):
3236
self.given_i_am_on_the_participant_record_page()
3337
self.then_the_accessibility_baseline_is_met()
3438

35-
def given_i_am_viewing_an_appointment(self):
39+
def test_viewing_upcoming_appointments(self):
40+
self.given_the_participant_has_an_upcoming_appointment()
41+
self.and_i_am_on_the_participant_record_page()
42+
self.then_i_should_see_the_upcoming_appointment()
43+
self.when_i_click_on_the_upcoming_appointment()
44+
self.then_i_should_be_on_the_upcoming_appointment_page()
45+
46+
def test_viewing_past_appointments(self):
47+
self.given_i_have_past_appointments()
48+
self.and_i_am_on_the_participant_record_page()
49+
self.then_i_should_see_the_past_appointments()
50+
self.when_i_click_on_a_past_appointment()
51+
self.then_i_should_be_on_the_past_appointment_page()
52+
53+
def given_the_participant_has_an_upcoming_appointment(self):
54+
clinic_slot = ClinicSlotFactory(
55+
starts_at=datetime(2025, 1, 2, 11, tzinfo=tz.utc)
56+
)
57+
screening_episode = ScreeningEpisodeFactory(participant=self.participant)
58+
self.upcoming_appointment = AppointmentFactory(
59+
clinic_slot=clinic_slot, screening_episode=screening_episode
60+
)
61+
62+
def given_i_have_past_appointments(self):
63+
clinic_slot_2022 = ClinicSlotFactory(
64+
starts_at=datetime(2022, 1, 2, 11, tzinfo=tz.utc)
65+
)
66+
clinic_slot_2019 = ClinicSlotFactory(
67+
starts_at=datetime(2019, 1, 2, 11, tzinfo=tz.utc)
68+
)
69+
self.past_appointments = [
70+
AppointmentFactory(
71+
clinic_slot=clinic_slot_2022,
72+
screening_episode=ScreeningEpisodeFactory(participant=self.participant),
73+
),
74+
AppointmentFactory(
75+
clinic_slot=clinic_slot_2019,
76+
screening_episode=ScreeningEpisodeFactory(participant=self.participant),
77+
),
78+
]
79+
80+
def given_i_am_viewing_the_upcoming_appointment(self):
3681
self.page.goto(
3782
self.live_server_url
3883
+ reverse(
3984
"mammograms:start_screening",
40-
kwargs={"id": self.appointment.pk},
85+
kwargs={"id": self.upcoming_appointment.pk},
4186
)
4287
)
4388

@@ -50,6 +95,8 @@ def given_i_am_on_the_participant_record_page(self):
5095
)
5196
)
5297

98+
and_i_am_on_the_participant_record_page = given_i_am_on_the_participant_record_page
99+
53100
def when_i_click_on_view_participant_record(self):
54101
self.page.get_by_text("View participant record").click()
55102

@@ -71,6 +118,44 @@ def when_i_click_on_the_back_link(self):
71118
def then_i_should_be_back_on_the_appointment(self):
72119
path = reverse(
73120
"mammograms:start_screening",
74-
kwargs={"id": self.appointment.pk},
121+
kwargs={"id": self.upcoming_appointment.pk},
122+
)
123+
expect(self.page).to_have_url(re.compile(path))
124+
125+
def then_i_should_see_the_upcoming_appointment(self):
126+
upcoming = self.page.get_by_test_id("upcoming-appointments-table")
127+
expect(upcoming).to_be_visible()
128+
appointment = upcoming.get_by_test_id("appointment-row")
129+
expect(appointment).to_be_visible()
130+
expect(appointment).to_contain_text("2 January 2025")
131+
132+
def then_i_should_see_the_past_appointments(self):
133+
past = self.page.get_by_test_id("past-appointments-table")
134+
expect(past).to_be_visible()
135+
appointments = past.get_by_test_id("appointment-row").all()
136+
assert len(appointments) == 2
137+
138+
expect(appointments[0]).to_contain_text("2 January 2022")
139+
expect(appointments[1]).to_contain_text("2 January 2019")
140+
141+
def when_i_click_on_the_upcoming_appointment(self):
142+
past = self.page.get_by_test_id("upcoming-appointments-table")
143+
past.get_by_text("View details").click()
144+
145+
def when_i_click_on_a_past_appointment(self):
146+
past = self.page.get_by_test_id("past-appointments-table")
147+
past.get_by_text("View details").first.click()
148+
149+
def then_i_should_be_on_the_past_appointment_page(self):
150+
path = reverse(
151+
"mammograms:start_screening",
152+
kwargs={"id": self.past_appointments[0].pk},
153+
)
154+
expect(self.page).to_have_url(re.compile(path))
155+
156+
def then_i_should_be_on_the_upcoming_appointment_page(self):
157+
path = reverse(
158+
"mammograms:start_screening",
159+
kwargs={"id": self.upcoming_appointment.pk},
75160
)
76161
expect(self.page).to_have_url(re.compile(path))

0 commit comments

Comments
 (0)