Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions manage_breast_screening/mammograms/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .appointment_cannot_go_ahead_form import AppointmentCannotGoAheadForm
from .appointment_note_form import AppointmentNoteForm
from .ask_for_medical_information_form import AskForMedicalInformationForm
from .breast_augmentation_history_form import BreastAugmentationHistoryForm
from .cyst_history_form import CystHistoryForm
Expand All @@ -15,6 +16,7 @@
__all__ = [
"AppointmentCannotGoAheadForm",
"AskForMedicalInformationForm",
"AppointmentNoteForm",
"BreastAugmentationHistoryForm",
"CystHistoryForm",
"RecordMedicalInformationForm",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django import forms
from django.forms import Textarea

from manage_breast_screening.nhsuk_forms.fields import CharField


class AppointmentNoteForm(forms.Form):
content = CharField(
label="Note",
hint="Include information that is relevant to this appointment.",
required=True,
error_messages={
"required": "Enter a note",
},
label_classes="nhsuk-label--m",
widget=Textarea(attrs={"rows": 5}),
)

def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None)
if self.instance:
initial = kwargs.setdefault("initial", {})
initial.setdefault("content", self.instance.content)
super().__init__(*args, **kwargs)

def save(self):
self.instance.content = self.cleaned_data["content"]
self.instance.save()
return self.instance
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{% extends 'layout-app.jinja' %}
{% from 'nhsuk/components/back-link/macro.jinja' import backLink %}
{% from 'nhsuk/components/button/macro.jinja' import button %}
{% from 'nhsuk/components/inset-text/macro.jinja' import insetText %}
{% from 'components/appointment-status/macro.jinja' import appointment_status %}
{% from 'components/appointment-header/macro.jinja' import appointment_header %}
{% from 'components/check-in/macro.jinja' import check_in %}
{% from 'components/secondary-navigation/macro.jinja' import app_secondary_navigation %}
{% from 'mammograms/special_appointments/special_appointment_banner.jinja' import special_appointment_banner %}
{% from 'django_form_helpers.jinja' import form_error_summary %}

{% block beforeContent %}
{{ backLink({
"href": presented_appointment.clinic_url,
"text": "Back to clinic"
}) }}
{% endblock beforeContent %}

{% block page_content %}
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-full">

{{ form_error_summary(form) }}

<div class="app-header">
<h1 class="nhsuk-heading-l">
<span class="nhsuk-caption-l">
{{ caption }}
</span>
{{ heading }}
</h1>

<div class="app-header__status-tag">
{{ appointment_status(presented_appointment) }}
<p class="nhsuk-u-margin-bottom-2">
{{ check_in(
presented_appointment,
check_in_url=url(
'mammograms:check_in',
kwargs={'pk': presented_appointment.pk}
),
csrf_input=csrf_input
) }}
</p>
</div>
</div>

{{ appointment_header(request.user, presented_appointment, csrf_input=csrf_input) }}

{{ special_appointment_banner(presented_appointment.special_appointment, show_change_link=presented_appointment.active) }}

{{ app_secondary_navigation({
"visuallyHiddenTitle": "Secondary menu",
"items": secondary_nav_items
}) }}

<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">

{% set inset_html %}
<p>
If adjustments are required,
<a href="{{ presented_appointment.special_appointment_url }}">
provide special appointment details instead
</a>
</p>
{% endset %}
{{ insetText({
"html": inset_html
}) }}

<form action="{{ request.path }}" method="POST" novalidate>
{{ csrf_input }}
{{ form.content.as_field_group() }}

<div class="nhsuk-button-group">
{{ button({
"text": "Save note"
}) }}
</div>
</form>
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def present_secondary_nav(pk, current_tab=None):
{
"id": "note",
"text": "Note",
"href": "#",
"href": reverse("mammograms:appointment_note", kwargs={"pk": pk}),
"current": current_tab == "note",
},
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pytest_django.asserts import assertContains, assertRedirects

from manage_breast_screening.core.models import AuditLog
from manage_breast_screening.participants.models import AppointmentNote
from manage_breast_screening.participants.tests.factories import AppointmentFactory


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


@pytest.mark.django_db
class TestAppointmentNoteView:
@pytest.mark.parametrize(
"client_fixture", ["clinical_user_client", "administrative_user_client"]
)
def test_users_can_save_note(self, request, client_fixture):
client = request.getfixturevalue(client_fixture)
appointment = AppointmentFactory.create(
clinic_slot__clinic__setting__provider=client.current_provider
)

note_content = "Participant prefers left arm blood pressure readings."
response = client.http.post(
reverse(
"mammograms:appointment_note",
kwargs={"pk": appointment.pk},
),
{"content": note_content},
)

assertRedirects(
response,
reverse("mammograms:appointment_note", kwargs={"pk": appointment.pk}),
)
saved_note = AppointmentNote.objects.get(appointment=appointment)
assert saved_note.content == note_content

@pytest.mark.parametrize(
"client_fixture", ["clinical_user_client", "administrative_user_client"]
)
def test_users_can_update_note(self, request, client_fixture):
client = request.getfixturevalue(client_fixture)
appointment = AppointmentFactory.create(
clinic_slot__clinic__setting__provider=client.current_provider
)
note = AppointmentNote.objects.create(
appointment=appointment, content="Original note"
)

updated_content = "Updated note content"
response = client.http.post(
reverse("mammograms:appointment_note", kwargs={"pk": appointment.pk}),
{"content": updated_content},
)

assertRedirects(
response,
reverse("mammograms:appointment_note", kwargs={"pk": appointment.pk}),
)
updated_note = AppointmentNote.objects.get(pk=note.pk)
assert updated_note.content == updated_content
assert AppointmentNote.objects.count() == 1


@pytest.mark.django_db
class TestConfirmIdentity:
def test_renders_response(self, clinical_user_client):
Expand Down
5 changes: 5 additions & 0 deletions manage_breast_screening/mammograms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
appointment_views.ParticipantDetails.as_view(),
name="participant_details",
),
path(
"<uuid:pk>/note/",
appointment_views.AppointmentNoteView.as_view(),
name="appointment_note",
),
path(
"<uuid:pk>/confirm-identity/",
appointment_views.ConfirmIdentity.as_view(),
Expand Down
50 changes: 45 additions & 5 deletions manage_breast_screening/mammograms/views/appointment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
)
from manage_breast_screening.participants.models import (
Appointment,
AppointmentNote,
Participant,
ParticipantReportedMammogram,
)
from manage_breast_screening.participants.presenters import ParticipantPresenter

from ..forms import (
AppointmentCannotGoAheadForm,
AppointmentNoteForm,
AskForMedicalInformationForm,
RecordMedicalInformationForm,
)
Expand Down Expand Up @@ -79,11 +81,6 @@ def get(self, request, *args, **kwargs):


class ParticipantDetails(AppointmentMixin, View):
"""
Show a completed appointment. Redirects to the start screening form
if the apppointment is in progress.
"""

template_name = "mammograms/show.jinja"

def get(self, request, *args, **kwargs):
Expand Down Expand Up @@ -118,6 +115,49 @@ def get(self, request, *args, **kwargs):
)


class AppointmentNoteView(AppointmentMixin, FormView):
template_name = "mammograms/show/appointment_note.jinja"
form_class = AppointmentNoteForm

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
appointment = self.appointment
appointment_presenter = AppointmentPresenter(
appointment, tab_description="Note"
)

context.update(
{
"heading": appointment_presenter.participant.full_name,
"caption": appointment_presenter.caption,
"page_title": appointment_presenter.page_title,
"presented_appointment": appointment_presenter,
"secondary_nav_items": present_secondary_nav(
appointment.pk, current_tab="note"
),
}
)
return context

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
try:
kwargs["instance"] = self.appointment.note
except AppointmentNote.DoesNotExist:
kwargs["instance"] = AppointmentNote(appointment=self.appointment)
return kwargs

def form_valid(self, form):
is_new_note = form.instance._state.adding
note = form.save()
auditor = Auditor.from_request(self.request)
if is_new_note:
auditor.audit_create(note)
else:
auditor.audit_update(note)
return redirect("mammograms:appointment_note", pk=self.appointment.pk)


class ConfirmIdentity(InProgressAppointmentMixin, TemplateView):
template_name = "mammograms/confirm_identity.jinja"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from manage_breast_screening.participants.models import (
Appointment,
AppointmentNote,
AppointmentStatus,
BenignLumpHistoryItem,
BreastAugmentationHistoryItem,
Expand Down Expand Up @@ -304,6 +305,7 @@ def create_reported_mammograms(self, participant, mammograms):
return participant_mammogram

def reset_db(self):
AppointmentNote.objects.all().delete()
UserAssignment.objects.all().delete()
Symptom.objects.all().delete()
BreastAugmentationHistoryItem.objects.all().delete()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.8 on 2025-12-02 14:21

import django.db.models.deletion
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('participants', '0048_alter_symptom_appointment'),
]

operations = [
migrations.CreateModel(
name='AppointmentNote',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('content', models.TextField(blank=True)),
('appointment', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='note', to='participants.appointment')),
],
options={
'abstract': False,
},
),
]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0048_alter_symptom_appointment
0049_appointmentnote
3 changes: 2 additions & 1 deletion manage_breast_screening/participants/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .appointment import Appointment, AppointmentStatus
from .appointment import Appointment, AppointmentNote, AppointmentStatus
from .benign_lump_history_item import BenignLumpHistoryItem
from .breast_augmentation_history_item import BreastAugmentationHistoryItem
from .breast_cancer_history_item import BreastCancerHistoryItem
Expand All @@ -14,6 +14,7 @@

__all__ = [
"Appointment",
"AppointmentNote",
"AppointmentStatus",
"BenignLumpHistoryItem",
"BreastAugmentationHistoryItem",
Expand Down
12 changes: 12 additions & 0 deletions manage_breast_screening/participants/models/appointment.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,15 @@ def is_in_progress(self):

def __str__(self):
return self.state


class AppointmentNote(BaseModel):
appointment = models.OneToOneField(
"participants.Appointment",
on_delete=models.PROTECT,
related_name="note",
)
content = models.TextField(blank=True)

def __str__(self):
return f"Note for appointment {self.appointment_id}"
8 changes: 8 additions & 0 deletions manage_breast_screening/participants/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ def current_status(obj, create, extracted, **kwargs):
)


class AppointmentNoteFactory(DjangoModelFactory):
class Meta:
model = models.AppointmentNote

appointment = SubFactory(AppointmentFactory)
content = Faker("sentence")


class BreastCancerHistoryItemFactory(DjangoModelFactory):
class Meta:
model = models.BreastCancerHistoryItem
Expand Down
Loading