Skip to content

Commit 8854484

Browse files
authored
Merge pull request #797 from NHSDigital/11596-add-appointment-note
Add appointment notes
2 parents 9546d02 + 452e313 commit 8854484

File tree

20 files changed

+357
-22
lines changed

20 files changed

+357
-22
lines changed

manage_breast_screening/mammograms/forms/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .appointment_cannot_go_ahead_form import AppointmentCannotGoAheadForm
2+
from .appointment_note_form import AppointmentNoteForm
23
from .ask_for_medical_information_form import AskForMedicalInformationForm
34
from .breast_augmentation_history_form import BreastAugmentationHistoryForm
45
from .cyst_history_form import CystHistoryForm
@@ -15,6 +16,7 @@
1516
__all__ = [
1617
"AppointmentCannotGoAheadForm",
1718
"AskForMedicalInformationForm",
19+
"AppointmentNoteForm",
1820
"BreastAugmentationHistoryForm",
1921
"CystHistoryForm",
2022
"RecordMedicalInformationForm",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django import forms
2+
from django.forms import Textarea
3+
4+
from manage_breast_screening.nhsuk_forms.fields import CharField
5+
6+
7+
class AppointmentNoteForm(forms.Form):
8+
content = CharField(
9+
label="Note",
10+
hint="Include information that is relevant to this appointment.",
11+
required=True,
12+
error_messages={
13+
"required": "Enter a note",
14+
},
15+
label_classes="nhsuk-label--m",
16+
widget=Textarea(attrs={"rows": 5}),
17+
)
18+
19+
def __init__(self, *args, **kwargs):
20+
self.instance = kwargs.pop("instance", None)
21+
if self.instance:
22+
initial = kwargs.setdefault("initial", {})
23+
initial.setdefault("content", self.instance.content)
24+
super().__init__(*args, **kwargs)
25+
26+
def save(self):
27+
self.instance.content = self.cleaned_data["content"]
28+
self.instance.save()
29+
return self.instance
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{% extends 'layout-app.jinja' %}
2+
{% from 'nhsuk/components/back-link/macro.jinja' import backLink %}
3+
{% from 'nhsuk/components/button/macro.jinja' import button %}
4+
{% from 'nhsuk/components/inset-text/macro.jinja' import insetText %}
5+
{% from 'components/appointment-status/macro.jinja' import appointment_status %}
6+
{% from 'components/appointment-header/macro.jinja' import appointment_header %}
7+
{% from 'components/check-in/macro.jinja' import check_in %}
8+
{% from 'components/secondary-navigation/macro.jinja' import app_secondary_navigation %}
9+
{% from 'mammograms/special_appointments/special_appointment_banner.jinja' import special_appointment_banner %}
10+
{% from 'django_form_helpers.jinja' import form_error_summary %}
11+
12+
{% block beforeContent %}
13+
{{ backLink({
14+
"href": presented_appointment.clinic_url,
15+
"text": "Back to clinic"
16+
}) }}
17+
{% endblock beforeContent %}
18+
19+
{% block page_content %}
20+
<div class="nhsuk-grid-row">
21+
<div class="nhsuk-grid-column-full">
22+
23+
{{ form_error_summary(form) }}
24+
25+
<div class="app-header">
26+
<h1 class="nhsuk-heading-l">
27+
<span class="nhsuk-caption-l">
28+
{{ caption }}
29+
</span>
30+
{{ heading }}
31+
</h1>
32+
33+
<div class="app-header__status-tag">
34+
{{ appointment_status(presented_appointment) }}
35+
<p class="nhsuk-u-margin-bottom-2">
36+
{{ check_in(
37+
presented_appointment,
38+
check_in_url=url(
39+
'mammograms:check_in',
40+
kwargs={'pk': presented_appointment.pk}
41+
),
42+
csrf_input=csrf_input
43+
) }}
44+
</p>
45+
</div>
46+
</div>
47+
48+
{{ appointment_header(request.user, presented_appointment, csrf_input=csrf_input) }}
49+
50+
{{ special_appointment_banner(presented_appointment.special_appointment, show_change_link=presented_appointment.active) }}
51+
52+
{{ app_secondary_navigation({
53+
"visuallyHiddenTitle": "Secondary menu",
54+
"items": secondary_nav_items
55+
}) }}
56+
57+
<div class="nhsuk-grid-row">
58+
<div class="nhsuk-grid-column-two-thirds">
59+
60+
{% set inset_html %}
61+
<p>
62+
If adjustments are required,
63+
<a href="{{ presented_appointment.special_appointment_url }}">
64+
provide special appointment details instead
65+
</a>
66+
</p>
67+
{% endset %}
68+
{{ insetText({
69+
"html": inset_html
70+
}) }}
71+
72+
<form action="{{ request.path }}" method="POST" novalidate>
73+
{{ csrf_input }}
74+
{{ form.content.as_field_group() }}
75+
76+
<div class="nhsuk-button-group">
77+
{{ button({
78+
"text": "Save note"
79+
}) }}
80+
</div>
81+
</form>
82+
</div>
83+
</div>
84+
{% endblock %}

manage_breast_screening/mammograms/presenters/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def present_secondary_nav(pk, current_tab=None):
4141
{
4242
"id": "note",
4343
"text": "Note",
44-
"href": "#",
44+
"href": reverse("mammograms:appointment_note", kwargs={"pk": pk}),
4545
"current": current_tab == "note",
4646
},
4747
]

manage_breast_screening/mammograms/tests/views/test_appointment_views.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pytest_django.asserts import assertContains, assertRedirects
44

55
from manage_breast_screening.core.models import AuditLog
6+
from manage_breast_screening.participants.models import AppointmentNote
67
from manage_breast_screening.participants.tests.factories import AppointmentFactory
78

89

@@ -21,6 +22,60 @@ def test_renders_response(self, clinical_user_client):
2122
assert response.status_code == 200
2223

2324

25+
@pytest.mark.django_db
26+
class TestAppointmentNoteView:
27+
@pytest.mark.parametrize(
28+
"client_fixture", ["clinical_user_client", "administrative_user_client"]
29+
)
30+
def test_users_can_save_note(self, request, client_fixture):
31+
client = request.getfixturevalue(client_fixture)
32+
appointment = AppointmentFactory.create(
33+
clinic_slot__clinic__setting__provider=client.current_provider
34+
)
35+
36+
note_content = "Participant prefers left arm blood pressure readings."
37+
response = client.http.post(
38+
reverse(
39+
"mammograms:appointment_note",
40+
kwargs={"pk": appointment.pk},
41+
),
42+
{"content": note_content},
43+
)
44+
45+
assertRedirects(
46+
response,
47+
reverse("mammograms:appointment_note", kwargs={"pk": appointment.pk}),
48+
)
49+
saved_note = AppointmentNote.objects.get(appointment=appointment)
50+
assert saved_note.content == note_content
51+
52+
@pytest.mark.parametrize(
53+
"client_fixture", ["clinical_user_client", "administrative_user_client"]
54+
)
55+
def test_users_can_update_note(self, request, client_fixture):
56+
client = request.getfixturevalue(client_fixture)
57+
appointment = AppointmentFactory.create(
58+
clinic_slot__clinic__setting__provider=client.current_provider
59+
)
60+
note = AppointmentNote.objects.create(
61+
appointment=appointment, content="Original note"
62+
)
63+
64+
updated_content = "Updated note content"
65+
response = client.http.post(
66+
reverse("mammograms:appointment_note", kwargs={"pk": appointment.pk}),
67+
{"content": updated_content},
68+
)
69+
70+
assertRedirects(
71+
response,
72+
reverse("mammograms:appointment_note", kwargs={"pk": appointment.pk}),
73+
)
74+
updated_note = AppointmentNote.objects.get(pk=note.pk)
75+
assert updated_note.content == updated_content
76+
assert AppointmentNote.objects.count() == 1
77+
78+
2479
@pytest.mark.django_db
2580
class TestConfirmIdentity:
2681
def test_renders_response(self, clinical_user_client):

manage_breast_screening/mammograms/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
appointment_views.ParticipantDetails.as_view(),
3737
name="participant_details",
3838
),
39+
path(
40+
"<uuid:pk>/note/",
41+
appointment_views.AppointmentNoteView.as_view(),
42+
name="appointment_note",
43+
),
3944
path(
4045
"<uuid:pk>/confirm-identity/",
4146
appointment_views.ConfirmIdentity.as_view(),

manage_breast_screening/mammograms/views/appointment_views.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
)
1414
from manage_breast_screening.participants.models import (
1515
Appointment,
16+
AppointmentNote,
1617
Participant,
1718
ParticipantReportedMammogram,
1819
)
1920
from manage_breast_screening.participants.presenters import ParticipantPresenter
2021

2122
from ..forms import (
2223
AppointmentCannotGoAheadForm,
24+
AppointmentNoteForm,
2325
AskForMedicalInformationForm,
2426
RecordMedicalInformationForm,
2527
)
@@ -79,11 +81,6 @@ def get(self, request, *args, **kwargs):
7981

8082

8183
class ParticipantDetails(AppointmentMixin, View):
82-
"""
83-
Show a completed appointment. Redirects to the start screening form
84-
if the apppointment is in progress.
85-
"""
86-
8784
template_name = "mammograms/show.jinja"
8885

8986
def get(self, request, *args, **kwargs):
@@ -118,6 +115,49 @@ def get(self, request, *args, **kwargs):
118115
)
119116

120117

118+
class AppointmentNoteView(AppointmentMixin, FormView):
119+
template_name = "mammograms/show/appointment_note.jinja"
120+
form_class = AppointmentNoteForm
121+
122+
def get_context_data(self, **kwargs):
123+
context = super().get_context_data(**kwargs)
124+
appointment = self.appointment
125+
appointment_presenter = AppointmentPresenter(
126+
appointment, tab_description="Note"
127+
)
128+
129+
context.update(
130+
{
131+
"heading": appointment_presenter.participant.full_name,
132+
"caption": appointment_presenter.caption,
133+
"page_title": appointment_presenter.page_title,
134+
"presented_appointment": appointment_presenter,
135+
"secondary_nav_items": present_secondary_nav(
136+
appointment.pk, current_tab="note"
137+
),
138+
}
139+
)
140+
return context
141+
142+
def get_form_kwargs(self):
143+
kwargs = super().get_form_kwargs()
144+
try:
145+
kwargs["instance"] = self.appointment.note
146+
except AppointmentNote.DoesNotExist:
147+
kwargs["instance"] = AppointmentNote(appointment=self.appointment)
148+
return kwargs
149+
150+
def form_valid(self, form):
151+
is_new_note = form.instance._state.adding
152+
note = form.save()
153+
auditor = Auditor.from_request(self.request)
154+
if is_new_note:
155+
auditor.audit_create(note)
156+
else:
157+
auditor.audit_update(note)
158+
return redirect("mammograms:appointment_note", pk=self.appointment.pk)
159+
160+
121161
class ConfirmIdentity(InProgressAppointmentMixin, TemplateView):
122162
template_name = "mammograms/confirm_identity.jinja"
123163

manage_breast_screening/nonprod/management/commands/seed_demo_data.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from manage_breast_screening.participants.models import (
2424
Appointment,
25+
AppointmentNote,
2526
AppointmentStatus,
2627
BenignLumpHistoryItem,
2728
BreastAugmentationHistoryItem,
@@ -304,6 +305,7 @@ def create_reported_mammograms(self, participant, mammograms):
304305
return participant_mammogram
305306

306307
def reset_db(self):
308+
AppointmentNote.objects.all().delete()
307309
UserAssignment.objects.all().delete()
308310
Symptom.objects.all().delete()
309311
BreastAugmentationHistoryItem.objects.all().delete()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.2.8 on 2025-12-02 14:21
2+
3+
import django.db.models.deletion
4+
import uuid
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('participants', '0048_alter_symptom_appointment'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='AppointmentNote',
17+
fields=[
18+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
19+
('created_at', models.DateTimeField(auto_now_add=True)),
20+
('updated_at', models.DateTimeField(auto_now=True)),
21+
('content', models.TextField(blank=True)),
22+
('appointment', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='note', to='participants.appointment')),
23+
],
24+
options={
25+
'abstract': False,
26+
},
27+
),
28+
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0048_alter_symptom_appointment
1+
0049_appointmentnote

0 commit comments

Comments
 (0)