Skip to content

Commit f168ff0

Browse files
authored
Merge pull request #938 from NHSDigital/DTOSS-11826-add-additional-image-details
Add additional image details
2 parents 224b065 + de774f1 commit f168ff0

File tree

14 files changed

+894
-12
lines changed

14 files changed

+894
-12
lines changed

manage_breast_screening/assets/sass/main.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,11 @@ a[href="#"] {
128128
@include nhsuk-typography-weight-bold($important: true);
129129
opacity: 0.9;
130130
}
131+
132+
.app-js-only {
133+
display: none;
134+
}
135+
136+
body.js-enabled .app-js-only {
137+
display: block;
138+
}

manage_breast_screening/mammograms/forms/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .add_image_details_form import AddImageDetailsForm
12
from .appointment_cannot_go_ahead_form import AppointmentCannotGoAheadForm
23
from .appointment_note_form import AppointmentNoteForm
34
from .appointment_proceed_anyway_form import AppointmentProceedAnywayForm
@@ -22,6 +23,7 @@
2223
)
2324

2425
__all__ = [
26+
"AddImageDetailsForm",
2527
"AppointmentCannotGoAheadForm",
2628
"AppointmentProceedAnywayForm",
2729
"AppointmentNoteForm",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from django.forms.widgets import Textarea
2+
3+
from manage_breast_screening.manual_images.services import StudyService
4+
from manage_breast_screening.nhsuk_forms.fields import CharField, IntegerField
5+
from manage_breast_screening.nhsuk_forms.forms import FormWithConditionalFields
6+
from manage_breast_screening.participants.models.appointment import (
7+
AppointmentWorkflowStepCompletion,
8+
)
9+
10+
11+
class AddImageDetailsForm(FormWithConditionalFields):
12+
rmlo_count = IntegerField(
13+
label="RMLO",
14+
classes="nhsuk-input--width-4",
15+
required=True,
16+
min_value=0,
17+
max_value=20,
18+
error_messages={
19+
"min_value": "Number of RMLO images must be at least 0.",
20+
"max_value": "Number of RMLO images must be at most 20.",
21+
"invalid": "Enter a valid number of RMLO images.",
22+
"required": "Enter the number of RMLO images.",
23+
},
24+
)
25+
rcc_count = IntegerField(
26+
label="RCC",
27+
classes="nhsuk-input--width-4",
28+
required=True,
29+
min_value=0,
30+
max_value=20,
31+
error_messages={
32+
"min_value": "Number of RCC images must be at least 0.",
33+
"max_value": "Number of RCC images must be at most 20.",
34+
"invalid": "Enter a valid number of RCC images.",
35+
"required": "Enter the number of RCC images.",
36+
},
37+
)
38+
right_eklund_count = IntegerField(
39+
label="Right Eklund",
40+
hint="Used with breast implants",
41+
classes="nhsuk-input--width-4",
42+
required=True,
43+
min_value=0,
44+
max_value=20,
45+
error_messages={
46+
"min_value": "Number of Right Eklund images must be at least 0.",
47+
"max_value": "Number of Right Eklund images must be at most 20.",
48+
"invalid": "Enter a valid number of Right Eklund images.",
49+
"required": "Enter the number of Right Eklund images.",
50+
},
51+
)
52+
lmlo_count = IntegerField(
53+
label="LMLO",
54+
classes="nhsuk-input--width-4",
55+
required=True,
56+
min_value=0,
57+
max_value=20,
58+
error_messages={
59+
"min_value": "Number of LMLO images must be at least 0.",
60+
"max_value": "Number of LMLO images must be at most 20.",
61+
"invalid": "Enter a valid number of LMLO images.",
62+
"required": "Enter the number of LMLO images.",
63+
},
64+
)
65+
lcc_count = IntegerField(
66+
label="LCC",
67+
classes="nhsuk-input--width-4",
68+
required=True,
69+
min_value=0,
70+
max_value=20,
71+
error_messages={
72+
"min_value": "Number of LCC images must be at least 0.",
73+
"max_value": "Number of LCC images must be at most 20.",
74+
"invalid": "Enter a valid number of LCC images.",
75+
"required": "Enter the number of LCC images.",
76+
},
77+
)
78+
left_eklund_count = IntegerField(
79+
label="Left Eklund",
80+
hint="Used with breast implants",
81+
classes="nhsuk-input--width-4",
82+
required=True,
83+
min_value=0,
84+
max_value=20,
85+
error_messages={
86+
"min_value": "Number of Left Eklund images must be at least 0.",
87+
"max_value": "Number of Left Eklund images must be at most 20.",
88+
"invalid": "Enter a valid number of Left Eklund images.",
89+
"required": "Enter the number of Left Eklund images.",
90+
},
91+
)
92+
93+
additional_details = CharField(
94+
hint="Provide information for image readers when reviewing",
95+
required=False,
96+
label="Notes for reader (optional)",
97+
label_classes="nhsuk-label--s",
98+
widget=Textarea(attrs={"rows": 2}),
99+
max_words=500,
100+
error_messages={"max_words": "Notes for reader must be 500 words or less"},
101+
)
102+
103+
def __init__(self, *args, **kwargs):
104+
super().__init__(*args, **kwargs)
105+
106+
def clean(self):
107+
cleaned_data = super().clean()
108+
109+
if (
110+
self.cleaned_data.get("rmlo_count") == 0
111+
and self.cleaned_data.get("rcc_count") == 0
112+
and self.cleaned_data.get("right_eklund_count") == 0
113+
and self.cleaned_data.get("lmlo_count") == 0
114+
and self.cleaned_data.get("lcc_count") == 0
115+
and self.cleaned_data.get("left_eklund_count") == 0
116+
):
117+
self.add_error(None, "Enter at least one image count")
118+
119+
return cleaned_data
120+
121+
def save(self, study_service: StudyService):
122+
return study_service.create(
123+
additional_details=self.cleaned_data.get("additional_details", ""),
124+
series_data=[
125+
self.build_series_dict("MLO", "R", "rmlo_count"),
126+
self.build_series_dict("CC", "R", "rcc_count"),
127+
self.build_series_dict("EKLUND", "R", "right_eklund_count"),
128+
self.build_series_dict("MLO", "L", "lmlo_count"),
129+
self.build_series_dict("CC", "L", "lcc_count"),
130+
self.build_series_dict("EKLUND", "L", "left_eklund_count"),
131+
],
132+
)
133+
134+
def build_series_dict(self, view_position, laterality, count_field_name):
135+
return {
136+
"view_position": view_position,
137+
"laterality": laterality,
138+
"count": self.cleaned_data.get(count_field_name),
139+
}
140+
141+
def mark_workflow_step_complete(self):
142+
self.appointment.completed_workflow_steps.get_or_create(
143+
step_name=AppointmentWorkflowStepCompletion.StepNames.TAKE_IMAGES,
144+
defaults={"created_by": self.request.user},
145+
)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
{% extends "workflow_step.jinja" %}
2+
{% from "nhsuk/components/button/macro.jinja" import button %}
3+
{% from "nhsuk/components/fieldset/macro.jinja" import fieldset %}
4+
5+
{% set active_workflow_step = 'TAKE_IMAGES' %}
6+
7+
{% block form %}
8+
<h2>How many images were taken?</h2>
9+
<div class="nhsuk-grid-row">
10+
<div class="nhsuk-grid-column-one-half">
11+
{% call fieldset({
12+
"legend": {
13+
"text": "<span class=\"nhsuk-u-visually-hidden\">How many images were taken for the </span>Right breast",
14+
"classes": "nhsuk-fieldset__legend--s"
15+
}
16+
}) %}
17+
{{ form.rmlo_count.as_field_group() }}
18+
{{ form.rcc_count.as_field_group() }}
19+
{{ form.right_eklund_count.as_field_group() }}
20+
{% endcall %}
21+
</div>
22+
<div class="nhsuk-grid-column-one-half">
23+
{% call fieldset({
24+
"legend": {
25+
"text": "<span class=\"nhsuk-u-visually-hidden\">How many images were taken for the </span>Left breast",
26+
"classes": "nhsuk-fieldset__legend--s"
27+
}
28+
}) %}
29+
{{ form.lmlo_count.as_field_group() }}
30+
{{ form.lcc_count.as_field_group() }}
31+
{{ form.left_eklund_count.as_field_group() }}
32+
{% endcall %}
33+
</div>
34+
</div>
35+
36+
<p class="app-js-only nhsuk-hint nhsuk-u-margin-bottom-4 nhsuk-u-font-size-22" id="total-images-count">
37+
Images taken: <span data-total-count></span>
38+
</p>
39+
40+
<div class="nhsuk-inset-text" id="repeat-images-inset" data-module="repeat-images-notice" style="display: none;">
41+
<span class="nhsuk-u-visually-hidden">Information: </span>
42+
<p>Repeat or extra image details captured on the next screen</p>
43+
</div>
44+
45+
{% call fieldset({
46+
"legend": {
47+
"text": "Additional details",
48+
"classes": "nhsuk-fieldset__legend--m"
49+
}
50+
}) %}
51+
{{ form.additional_details.as_field_group() }}
52+
{% endcall %}
53+
{% endblock %}
54+
55+
{% block bodyEnd %}
56+
{{ super() }}
57+
58+
<script>
59+
document.addEventListener('DOMContentLoaded', () => {
60+
const insetText = document.querySelector('[data-module="repeat-images-notice"]')
61+
const totalCountElement = document.querySelector('[data-total-count]')
62+
63+
const getCounts = () => {
64+
const countInputs = document.querySelectorAll('input[id$="_count"]')
65+
const counts = []
66+
for (const input of countInputs) {
67+
let count = input.value === '' ? 0 : parseInt(input.value, 10)
68+
counts.push(count)
69+
}
70+
return counts
71+
}
72+
73+
const getTotalCount = () => {
74+
let total = 0
75+
for (const count of getCounts()) {
76+
// if any count is not a number then display no total count, rather than a misleading one
77+
if (isNaN(count)) {
78+
return ''
79+
}
80+
81+
total += count
82+
}
83+
return total
84+
}
85+
86+
const hasRepeats = () => {
87+
for (const count of getCounts()) {
88+
if (!isNaN(count) && count > 1) {
89+
return true
90+
}
91+
}
92+
return false
93+
}
94+
95+
const updateTotalCount = () => {
96+
totalCountElement.textContent = getTotalCount()
97+
}
98+
99+
const updateVisibility = () => {
100+
if (hasRepeats()) {
101+
insetText.style.display = ''
102+
} else {
103+
insetText.style.display = 'none'
104+
}
105+
}
106+
107+
const updateAll = () => {
108+
updateTotalCount()
109+
updateVisibility()
110+
}
111+
112+
// Set initial state after a short delay to allow conditional reveals to initialize
113+
setTimeout(updateAll, 100)
114+
115+
const eventListener = (event) => {
116+
const target = event.target
117+
if (target.id && target.id.endsWith('_count')) {
118+
updateAll()
119+
}
120+
}
121+
document.addEventListener('input', eventListener)
122+
document.addEventListener('change', eventListener)
123+
})
124+
</script>
125+
{% endblock %}

0 commit comments

Comments
 (0)