Skip to content

Commit be3641a

Browse files
authored
Merge pull request #30 from NHSDigital/8441-appointment-cannot-proceed
Add appointment cannot go ahead page
2 parents 019076e + 6917768 commit be3641a

17 files changed

Lines changed: 967 additions & 27 deletions

.github/workflows/stage-2-test.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ jobs:
6666
cache-dependency-path: ./poetry.lock
6767

6868
- name: Install dependencies
69-
run:
70-
make dependencies
69+
run: make dependencies
70+
71+
- name: Install Playwright browsers
72+
run: poetry run playwright install chromium --with-deps
7173

7274
- name: "Run unit test suite"
73-
run: |
74-
make test-unit
75+
run: make test-unit
7576
env:
7677
DATABASE_NAME: postgres
7778
DATABASE_PASSWORD: postgres
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.8 on 2025-05-01 14:36
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('clinics', '0006_alter_appointment_status'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='appointment',
15+
name='reinvite',
16+
field=models.BooleanField(default=False),
17+
),
18+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.8 on 2025-05-06 12:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('clinics', '0007_appointment_reinvite'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='appointment',
15+
name='stopped_reasons',
16+
field=models.JSONField(blank=True, null=True),
17+
),
18+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generated by Django 5.2.1 on 2025-05-14 11:40
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('clinics', '0007_alter_appointment_status'),
10+
('clinics', '0008_appointment_stopped_reasons'),
11+
]
12+
13+
operations = [
14+
]

manage_breast_screening/clinics/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,6 @@ class Status:
162162
status = models.CharField(
163163
choices=STATUS_CHOICES, max_length=50, default=Status.CONFIRMED
164164
)
165+
reinvite = models.BooleanField(default=False)
166+
stopped_reasons = models.JSONField(null=True, blank=True)
167+

manage_breast_screening/config/jinja2_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def format_age(value: int) -> str:
7575

7676

7777
def environment(**options):
78-
env = Environment(**options)
78+
env = Environment(**options, extensions=["jinja2.ext.do"])
7979
if env.loader:
8080
env.loader = ChoiceLoader([PackageLoader("nhsuk_frontend_jinja"), env.loader])
8181

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
2+
import os
3+
from playwright.sync_api import sync_playwright, expect
4+
5+
class SystemTestCase(StaticLiveServerTestCase):
6+
@classmethod
7+
def setUpClass(cls):
8+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
9+
super().setUpClass()
10+
cls.playwright = sync_playwright().start()
11+
cls.browser = cls.playwright.chromium.launch()
12+
13+
@classmethod
14+
def tearDownClass(cls):
15+
super().tearDownClass()
16+
cls.browser.close()
17+
cls.playwright.stop()
18+
19+
def setUp(self):
20+
self.page = self.browser.new_page()
21+
22+
def tearDown(self):
23+
self.page.close()

manage_breast_screening/record_a_mammogram/forms.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from django import forms
22

3-
43
class ScreeningAppointmentForm(forms.Form):
54
decision = forms.ChoiceField(
65
choices=(
@@ -44,4 +43,67 @@ def save(self):
4443

4544

4645
class AppointmentCannotGoAheadForm(forms.Form):
47-
pass
46+
STOPPED_REASON_CHOICES = (
47+
("participant_did_not_attend", "Participant did not attend"),
48+
("failed_identity_check", "Failed identity check"),
49+
("language_difficulties", "Language difficulties"),
50+
("physical_health_issue", "Physical health issue"),
51+
("mental_health_issue", "Mental health issue"),
52+
("last_mammogram_within_6_months", "Last mammogram within 6 months"),
53+
("breast_implant_risks", "Breast implant risks"),
54+
("pain_during_screening", "Pain during screening"),
55+
("technical_issues", "Technical issues"),
56+
("participant_withdrew_consent", "Participant withdrew consent"),
57+
("other", "Other"),
58+
)
59+
60+
def __init__(self, *args, **kwargs):
61+
if 'instance' not in kwargs:
62+
raise ValueError("AppointmentCannotGoAheadForm requires an instance")
63+
self.instance = kwargs.pop('instance')
64+
super().__init__(*args, **kwargs)
65+
66+
# Dynamically add detail fields for each choice
67+
for field_name, _ in self.STOPPED_REASON_CHOICES:
68+
self.fields[f"{field_name}_details"] = forms.CharField(required=False)
69+
70+
stopped_reasons = forms.MultipleChoiceField(
71+
choices=STOPPED_REASON_CHOICES,
72+
required=True,
73+
error_messages={
74+
"required": "A reason for why this appointment cannot continue must be provided"
75+
}
76+
)
77+
78+
decision = forms.ChoiceField(
79+
choices=(
80+
("True", "Yes, add participant to reinvite list"),
81+
("False", "No"),
82+
),
83+
required=True,
84+
widget=forms.RadioSelect(),
85+
error_messages={
86+
"required": "Select whether the participant needs to be invited for another appointment"
87+
}
88+
)
89+
90+
def clean(self):
91+
cleaned_data = super().clean()
92+
93+
if 'stopped_reasons' in cleaned_data and 'other' in cleaned_data['stopped_reasons']:
94+
if not cleaned_data.get('other_details'):
95+
self.add_error('other_details', 'Explain why this appointment cannot proceed')
96+
return cleaned_data
97+
98+
def save(self):
99+
reasons_json = {}
100+
reasons_json["stopped_reasons"] = self.cleaned_data["stopped_reasons"]
101+
for field_name, value in self.cleaned_data.items():
102+
if field_name.endswith('_details') and value:
103+
reasons_json[field_name] = value
104+
self.instance.stopped_reasons = reasons_json
105+
self.instance.reinvite = self.cleaned_data["decision"]
106+
self.instance.status = self.instance.Status.ATTENDED_NOT_SCREENED
107+
self.instance.save()
108+
109+
return self.instance

manage_breast_screening/record_a_mammogram/tests/system/__init__.py

Whitespace-only changes.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import re
2+
from django.urls import reverse
3+
from playwright.sync_api import expect
4+
5+
from manage_breast_screening.config.system_test_setup import SystemTestCase
6+
from manage_breast_screening.clinics.tests.factories import AppointmentFactory, ScreeningEpisodeFactory
7+
from manage_breast_screening.participants.tests.factories import ParticipantFactory
8+
from manage_breast_screening.clinics.models import Appointment
9+
10+
class UserSubmitsCannotGoAheadForm(SystemTestCase):
11+
def setUp(self):
12+
super().setUp()
13+
self.participant = ParticipantFactory()
14+
self.screening_episode = ScreeningEpisodeFactory(participant=self.participant)
15+
self.appointment = AppointmentFactory(screening_episode=self.screening_episode)
16+
17+
def test_user_submits_cannot_go_ahead_form(self):
18+
self.given_i_am_on_the_cannot_go_ahead_form()
19+
self.when_i_submit_the_form()
20+
self.then_i_should_see_validation_errors()
21+
22+
self.when_i_select_a_reason_for_the_appointment_being_stopped()
23+
self.and_i_select_other_as_a_reason()
24+
self.and_i_choose_to_add_the_participant_to_the_reinvite_list()
25+
self.when_i_submit_the_form()
26+
self.then_i_see_an_error_for_other_details()
27+
28+
self.when_i_fill_in_other_details()
29+
self.when_i_submit_the_form()
30+
self.then_i_see_the_clinics_page()
31+
self.and_the_appointment_is_updated()
32+
33+
34+
def given_i_am_on_the_cannot_go_ahead_form(self):
35+
self.page.goto(
36+
self.live_server_url + reverse(
37+
"record_a_mammogram:appointment_cannot_go_ahead", kwargs={"id": self.appointment.pk}
38+
)
39+
)
40+
41+
def when_i_submit_the_form(self):
42+
self.page.get_by_role("button", name="Continue").click()
43+
44+
def then_i_should_see_validation_errors(self):
45+
expect(
46+
self.page.locator("#stopped_reasons-error")
47+
).to_have_text(re.compile("A reason for why this appointment cannot continue must be provided"))
48+
49+
def when_i_select_a_reason_for_the_appointment_being_stopped(self):
50+
self.page.get_by_label("Failed identity check").check()
51+
52+
def and_i_select_other_as_a_reason(self):
53+
self.page.get_by_label("Other").check()
54+
55+
def and_i_choose_to_add_the_participant_to_the_reinvite_list(self):
56+
self.page.get_by_label("Yes, add participant to reinvite list").click()
57+
expect(self.page.get_by_label("Yes, add participant to reinvite list")).to_be_checked()
58+
59+
def then_i_see_an_error_for_other_details(self):
60+
expect(
61+
self.page.locator("#other_details-error")
62+
).to_have_text(re.compile("Explain why this appointment cannot proceed"))
63+
64+
def when_i_fill_in_other_details(self):
65+
self.page.locator("#other_details").fill("Explain other choice")
66+
67+
def then_i_see_the_clinics_page(self):
68+
expect(self.page).to_have_url(
69+
re.compile(reverse("clinics:index"))
70+
)
71+
72+
def and_the_appointment_is_updated(self):
73+
self.appointment.refresh_from_db()
74+
self.assertEqual(self.appointment.status, Appointment.Status.ATTENDED_NOT_SCREENED)

0 commit comments

Comments
 (0)