Skip to content

Commit 7a99d06

Browse files
authored
Merge pull request #824 from NHSDigital/DTOSS-11781-cleanup-3
refactor medical history views
2 parents 8710e0e + bd1a084 commit 7a99d06

File tree

41 files changed

+591
-960
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+591
-960
lines changed

manage_breast_screening/core/tests/views/__init__.py

Whitespace-only changes.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from unittest.mock import MagicMock
2+
3+
import pytest
4+
from django import forms
5+
from django.contrib.contenttypes.models import ContentType
6+
from django.contrib.messages.middleware import MessageMiddleware
7+
from django.contrib.sessions.middleware import SessionMiddleware
8+
from django.test import RequestFactory
9+
10+
from manage_breast_screening.auth.tests.factories import UserFactory
11+
from manage_breast_screening.core.models import AuditLog
12+
from manage_breast_screening.core.views.generic import (
13+
AddWithAuditView,
14+
UpdateWithAuditView,
15+
)
16+
from manage_breast_screening.users.models import User
17+
18+
19+
def apply_middleware(request):
20+
for middleware_class in [SessionMiddleware, MessageMiddleware]:
21+
middleware = middleware_class(get_response=MagicMock())
22+
middleware.process_request(request)
23+
return request
24+
25+
26+
class DummyForm(forms.Form):
27+
first_name = forms.CharField()
28+
last_name = forms.CharField()
29+
30+
def __init__(self, *args, instance=None, **kwargs):
31+
self.instance = instance
32+
super().__init__(*args, **kwargs)
33+
34+
def create(self):
35+
return UserFactory.create(
36+
first_name=self.cleaned_data["first_name"],
37+
last_name=self.cleaned_data["last_name"],
38+
nhs_uid="create_uid",
39+
)
40+
41+
def update(self):
42+
return UserFactory.create(
43+
first_name=self.cleaned_data["first_name"],
44+
last_name=self.cleaned_data["last_name"],
45+
nhs_uid="update_uid",
46+
)
47+
48+
49+
class AddView(AddWithAuditView):
50+
form_class = DummyForm
51+
thing_name = "test"
52+
success_url = "/success"
53+
54+
55+
class UpdateView(UpdateWithAuditView):
56+
form_class = DummyForm
57+
thing_name = "test"
58+
success_url = "/success"
59+
60+
61+
class TestAddWithAuditView:
62+
def test_success_message_content(self):
63+
obj = UserFactory.build()
64+
65+
assert AddView().get_success_message_content(obj) == "Added test"
66+
67+
def test_get_context_data(self):
68+
request = RequestFactory().get("/")
69+
context = AddView(request=request).get_context_data()
70+
71+
assert context["heading"] == "Add test"
72+
assert context["page_title"] == "Add test"
73+
74+
@pytest.mark.django_db
75+
def test_audits_if_form_valid(self):
76+
request = apply_middleware(RequestFactory().post("/"))
77+
request.user = UserFactory()
78+
form = DummyForm({"first_name": "abc", "last_name": "def"})
79+
80+
assert form.is_valid()
81+
AddView(request=request).form_valid(form)
82+
83+
last_audit = AuditLog.objects.last()
84+
assert last_audit is not None
85+
assert last_audit.content_type == ContentType.objects.get_for_model(User)
86+
assert last_audit.actor == request.user
87+
assert last_audit.operation == AuditLog.Operations.CREATE
88+
assert last_audit.snapshot == {
89+
"email": "",
90+
"first_name": "abc",
91+
"last_name": "def",
92+
"is_staff": False,
93+
"is_active": True,
94+
"last_login": None,
95+
"nhs_uid": "create_uid",
96+
}
97+
assert last_audit.system_update_id is None
98+
99+
100+
class TestUpdateWithAuditView:
101+
def test_success_message_content(self):
102+
obj = UserFactory.build()
103+
104+
assert UpdateView().get_success_message_content(obj) == "Updated test"
105+
106+
def test_get_context_data(self):
107+
obj = UserFactory.build()
108+
request = RequestFactory().get("/")
109+
view = UpdateView(request=request)
110+
view.object = obj
111+
112+
context = view.get_context_data()
113+
114+
assert context["heading"] == "Edit test"
115+
assert context["page_title"] == "Edit test"
116+
117+
@pytest.mark.django_db
118+
def test_audits_if_form_valid(self):
119+
request = apply_middleware(RequestFactory().post("/"))
120+
request.user = UserFactory()
121+
form = DummyForm({"first_name": "new", "last_name": "name"})
122+
123+
assert form.is_valid()
124+
UpdateView(request=request).form_valid(form)
125+
126+
last_audit = AuditLog.objects.last()
127+
assert last_audit is not None
128+
assert last_audit.content_type == ContentType.objects.get_for_model(User)
129+
assert last_audit.actor == request.user
130+
assert last_audit.operation == AuditLog.Operations.UPDATE
131+
assert last_audit.snapshot == {
132+
"email": "",
133+
"first_name": "new",
134+
"last_name": "name",
135+
"is_staff": False,
136+
"is_active": True,
137+
"last_login": None,
138+
"nhs_uid": "update_uid",
139+
}
140+
assert last_audit.system_update_id is None

manage_breast_screening/core/views/generic.py

Lines changed: 171 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,174 @@
1+
import logging
2+
13
from django.contrib import messages
4+
from django.http import Http404
25
from django.shortcuts import redirect
3-
from django.views.generic import DeleteView
6+
from django.views.generic import DeleteView, FormView
7+
from django.views.generic.detail import SingleObjectMixin
48

59
from ..services.auditor import Auditor
610

11+
logger = logging.getLogger(__name__)
12+
13+
14+
class NamedThingMixin:
15+
"""
16+
A helper to generate consistent messages and titles.
17+
If needed, individual messages can be overriden in subclasses.
18+
"""
19+
20+
thing_name = None
21+
22+
def add_title(self, thing_name):
23+
return f"Add {thing_name}"
24+
25+
def added_message(self, thing_name):
26+
return f"Added {thing_name}"
27+
28+
def update_title(self, thing_name):
29+
return f"Edit {thing_name}"
30+
31+
def updated_message(self, thing_name):
32+
return f"Updated {thing_name}"
33+
34+
def delete_button_text(self, thing_name):
35+
return f"Delete {thing_name}"
36+
37+
def deleted_message_text(self, thing_name):
38+
return f"Deleted {thing_name}"
39+
40+
def confirm_delete_link_text(self, thing_name):
41+
return f"Delete this {thing_name}"
42+
43+
def get_thing_name(self, instance=None):
44+
"""
45+
The name of the thing in lowercase, e.g. "appointment"
46+
"""
47+
if self.thing_name:
48+
return self.thing_name
49+
50+
raise ValueError("thing_name is unset")
51+
52+
53+
class AddWithAuditView(NamedThingMixin, FormView):
54+
"""
55+
A generic view that adds an object, similar to django.views.generic.CreateView but not based on ModelForms.
56+
An audit record is created and a success message is shown on redirect.
57+
58+
If valid, the form's create() method will be called.
59+
"""
60+
61+
template_name = "layout-form.jinja"
62+
63+
def get_success_message_content(self, object):
64+
return self.added_message(thing_name=self.get_thing_name(object))
65+
66+
def get_context_data(self, **kwargs):
67+
context = super().get_context_data()
68+
69+
title = self.add_title(thing_name=self.get_thing_name())
70+
71+
context.update(
72+
{
73+
"heading": title,
74+
"page_title": title,
75+
},
76+
)
77+
78+
return context
79+
80+
def get_create_kwargs(self):
81+
return {}
82+
83+
def form_valid(self, form):
84+
created_object = form.create(**self.get_create_kwargs())
85+
86+
auditor = Auditor.from_request(self.request)
87+
auditor.audit_create(created_object)
88+
89+
messages.add_message(
90+
self.request,
91+
messages.SUCCESS,
92+
self.get_success_message_content(created_object),
93+
)
94+
95+
return super().form_valid(form)
96+
97+
98+
class UpdateWithAuditView(NamedThingMixin, SingleObjectMixin, FormView):
99+
"""
100+
A generic view that updates an object, similar to django.views.generic.UpdateView but not based on ModelForms.
101+
An audit record is created and a success message is shown on redirect.
102+
103+
The form's constructor is expected to have an `instance` parameter.
104+
If valid, the form's update() method will be called.
105+
"""
106+
107+
template_name = "layout-form.jinja"
108+
109+
def get_success_message_content(self, object):
110+
return self.updated_message(thing_name=self.get_thing_name(object))
111+
112+
def get_form_kwargs(self):
113+
kwargs = super().get_form_kwargs()
114+
kwargs["instance"] = self.object
115+
return kwargs
116+
117+
def get(self, request, *args, **kwargs):
118+
self.object = self.get_object()
119+
if self.object is None:
120+
logger.warning("Object does not exist, redirecting to success URL")
121+
return redirect(self.get_success_url())
122+
return super().get(request, *args, **kwargs)
123+
124+
def post(self, request, *args, **kwargs):
125+
self.object = self.get_object()
126+
if self.object is None:
127+
raise Http404
128+
return super().post(request, *args, **kwargs)
129+
130+
def get_delete_url(self):
131+
return None
132+
133+
def get_context_data(self, **kwargs):
134+
context = super().get_context_data()
135+
136+
thing_name = self.get_thing_name(self.object)
137+
title = self.update_title(thing_name=thing_name)
138+
139+
delete_href = self.get_delete_url()
140+
if delete_href:
141+
context["delete_link"] = {
142+
"text": self.confirm_delete_link_text(thing_name=thing_name),
143+
"class": "nhsuk-link app-link--warning",
144+
"href": delete_href,
145+
}
146+
147+
context.update(
148+
{
149+
"heading": title,
150+
"page_title": title,
151+
},
152+
)
153+
154+
return context
155+
156+
def form_valid(self, form):
157+
created_object = form.update()
7158

8-
class DeleteWithAuditView(DeleteView):
159+
auditor = Auditor.from_request(self.request)
160+
auditor.audit_update(created_object)
161+
162+
messages.add_message(
163+
self.request,
164+
messages.SUCCESS,
165+
self.get_success_message_content(created_object),
166+
)
167+
168+
return super().form_valid(form)
169+
170+
171+
class DeleteWithAuditView(NamedThingMixin, DeleteView):
9172
"""
10173
A generic delete view with a confirmation page, success message,
11174
and some default context variables.
@@ -14,25 +177,24 @@ class DeleteWithAuditView(DeleteView):
14177
"""
15178

16179
template_name = "layout-confirmation.jinja"
17-
18-
def get_thing_name(self, object):
19-
return object._meta.verbose_name
180+
title_pattern = "Are you sure you want to delete this {thing_name}"
20181

21182
def get_success_message_content(self, object):
22-
return f"Deleted {self.get_thing_name(object)}"
183+
return self.deleted_message_text(thing_name=self.get_thing_name(object))
23184

24185
def get_cancel_url(self):
25186
return self.get_success_url()
26187

27188
def get_context_data(self, **kwargs):
28189
context = super().get_context_data(**kwargs)
29190
thing_name = self.get_thing_name(self.object)
191+
title = self.title_pattern.format(thing_name=thing_name)
30192
context.update(
31193
{
32-
"page_title": f"Are you sure you want to delete this {thing_name}?",
33-
"heading": f"Are you sure you want to delete this {thing_name}?",
194+
"page_title": title,
195+
"heading": title,
34196
"confirm_action": {
35-
"text": f"Delete {thing_name}",
197+
"text": self.delete_button_text(thing_name=thing_name),
36198
"href": self.request.get_full_path(),
37199
},
38200
"cancel_action": {"href": self.get_cancel_url()},

manage_breast_screening/mammograms/forms/medical_history/benign_lump_history_item_form.py

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

3-
from manage_breast_screening.core.services.auditor import Auditor
43
from manage_breast_screening.nhsuk_forms.fields.char_field import CharField
54
from manage_breast_screening.nhsuk_forms.fields.choice_fields import (
65
ChoiceField,
@@ -132,9 +131,7 @@ def initial_values(self, instance):
132131

133132
return initial
134133

135-
def create(self, appointment, request):
136-
auditor = Auditor.from_request(request)
137-
134+
def create(self, appointment):
138135
benign_lump_history_item = BenignLumpHistoryItem.objects.create(
139136
appointment=appointment,
140137
left_breast_procedures=self.cleaned_data.get("left_breast_procedures", []),
@@ -147,16 +144,12 @@ def create(self, appointment, request):
147144
additional_details=self.cleaned_data.get("additional_details", ""),
148145
)
149146

150-
auditor.audit_create(benign_lump_history_item)
151-
152147
return benign_lump_history_item
153148

154-
def update(self, request):
149+
def update(self):
155150
if self.instance is None:
156151
raise ValueError("Form has no instance")
157152

158-
auditor = Auditor.from_request(request)
159-
160153
self.instance.left_breast_procedures = self.cleaned_data.get(
161154
"left_breast_procedures", []
162155
)
@@ -171,7 +164,6 @@ def update(self, request):
171164
)
172165

173166
self.instance.save()
174-
auditor.audit_update(self.instance)
175167

176168
return self.instance
177169

0 commit comments

Comments
 (0)