Skip to content

Commit ae8c843

Browse files
committed
Extract presenters for clinics page
1 parent 4963fd3 commit ae8c843

9 files changed

Lines changed: 191 additions & 117 deletions

File tree

manage_breast_screening/clinics/models.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import date
2+
from enum import StrEnum
23

34
from django.db import models
45

@@ -20,7 +21,27 @@ class Setting(BaseModel):
2021
provider = models.ForeignKey(Provider, on_delete=models.CASCADE)
2122

2223

24+
class ClinicFilter(StrEnum):
25+
TODAY = "today"
26+
UPCOMING = "upcoming"
27+
COMPLETED = "completed"
28+
ALL = "all"
29+
30+
2331
class ClinicQuerySet(models.QuerySet):
32+
def by_filter(self, filter: str):
33+
match filter:
34+
case ClinicFilter.TODAY:
35+
return self.today()
36+
case ClinicFilter.UPCOMING:
37+
return self.upcoming()
38+
case ClinicFilter.COMPLETED:
39+
return self.completed()
40+
case ClinicFilter.ALL:
41+
return self
42+
case _:
43+
raise ValueError(filter)
44+
2445
def today(self):
2546
"""
2647
Clinics that start today
@@ -100,6 +121,15 @@ def session_type(self):
100121
def time_range(self):
101122
return {"start_time": self.starts_at, "end_time": self.ends_at}
102123

124+
@classmethod
125+
def filter_counts(cls):
126+
return {
127+
ClinicFilter.ALL: cls.objects.count(),
128+
ClinicFilter.TODAY: cls.objects.today().count(),
129+
ClinicFilter.UPCOMING: cls.objects.upcoming().count(),
130+
ClinicFilter.COMPLETED: cls.objects.completed().count(),
131+
}
132+
103133

104134
class ClinicSlot(BaseModel):
105135
clinic = models.ForeignKey(
@@ -164,4 +194,3 @@ class Status:
164194
)
165195
reinvite = models.BooleanField(default=False)
166196
stopped_reasons = models.JSONField(null=True, blank=True)
167-
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from ..utils.date_formatting import format_date, format_time_range
2+
from ..utils.string_formatting import sentence_case
3+
from .models import Clinic
4+
5+
6+
class ClinicsPresenter:
7+
def __init__(self, filtered_clinics, filter, counts_by_filter):
8+
self.clinics = [ClinicPresenter(clinic) for clinic in filtered_clinics]
9+
self.counts_by_filter = counts_by_filter
10+
self.filter = filter
11+
12+
@property
13+
def heading(self):
14+
if self.filter == "today":
15+
return "Today’s clinics"
16+
elif self.filter == "upcoming":
17+
return "Upcoming clinics"
18+
elif self.filter == "completed":
19+
return "Completed clinics this week"
20+
else:
21+
return "All clinics this week"
22+
23+
24+
class ClinicPresenter:
25+
STATUS_COLORS = {
26+
Clinic.State.SCHEDULED: "blue", # default blue
27+
Clinic.State.IN_PROGRESS: "blue",
28+
Clinic.State.CLOSED: "grey",
29+
}
30+
31+
def __init__(self, clinic):
32+
self._clinic = clinic
33+
self.starts_at = format_date(clinic.starts_at)
34+
self.session_type = clinic.session_type().capitalize()
35+
self.number_of_slots = clinic.clinic_slots.count()
36+
self.location_name = sentence_case(clinic.setting.name)
37+
self.time_range = format_time_range(clinic.time_range())
38+
self.type = clinic.get_type_display()
39+
self.risk_type = clinic.get_risk_type_display()
40+
41+
@property
42+
def state(self):
43+
return {
44+
"text": self._clinic.get_state_display(),
45+
"classes": "nhsuk-tag--" + self.STATUS_COLORS[self._clinic.state],
46+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from datetime import datetime
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
6+
from manage_breast_screening.clinics.presenters import ClinicPresenter
7+
8+
from ..models import Clinic
9+
10+
11+
@pytest.fixture
12+
def mock_clinic():
13+
mock = MagicMock(spec=Clinic)
14+
15+
mock.starts_at = datetime(2025, 1, 1, 9)
16+
mock.session_type.return_value = "All day"
17+
mock.clinic_slots.count.return_value = 10
18+
mock.setting.name = "Test setting"
19+
mock.time_range.return_value = {
20+
"start_time": datetime(2025, 1, 1, 9),
21+
"end_time": datetime(2025, 1, 1, 15),
22+
}
23+
mock.get_type_display.return_value = "Screening"
24+
mock.get_risk_type_display.return_value = "Routine"
25+
26+
return mock
27+
28+
29+
def test_clinic_presenter(mock_clinic):
30+
presenter = ClinicPresenter(mock_clinic)
31+
32+
assert presenter.starts_at == "1 January 2025"
33+
assert presenter.session_type == "All day"
34+
assert presenter.number_of_slots == 10
35+
assert presenter.location_name == "Test setting"
36+
assert presenter.time_range == "9am to 3pm"
37+
assert presenter.type == "Screening"
38+
assert presenter.risk_type == "Routine"
Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from datetime import datetime
2-
31
from django.shortcuts import render
42

5-
from .models import Clinic, Provider, Setting
3+
from manage_breast_screening.clinics.presenters import ClinicsPresenter
4+
5+
from .models import Clinic
66

77
STATUS_COLORS = {
88
Clinic.State.SCHEDULED: "blue", # default blue
@@ -12,28 +12,12 @@
1212

1313

1414
def clinic_list(request, filter="today"):
15-
match filter:
16-
case "today":
17-
clinics = Clinic.objects.today()
18-
case "upcoming":
19-
clinics = Clinic.objects.upcoming()
20-
case "completed":
21-
clinics = Clinic.objects.completed()
22-
case _:
23-
clinics = Clinic.objects.all()
15+
clinics = Clinic.objects.prefetch_related("setting").by_filter(filter)
16+
counts_by_filter = Clinic.filter_counts()
17+
presenter = ClinicsPresenter(clinics, filter, counts_by_filter)
2418

2519
return render(
2620
request,
2721
"clinics/index.html",
28-
context={
29-
"filter": filter,
30-
"filteredClinics": clinics,
31-
"filteredClinicCounts": {
32-
"all": Clinic.objects.count(),
33-
"today": Clinic.objects.today().count(),
34-
"upcoming": Clinic.objects.upcoming().count(),
35-
"completed": Clinic.objects.completed().count(),
36-
},
37-
"STATUS_COLORS": STATUS_COLORS,
38-
},
22+
context={"presenter": presenter},
3923
)

manage_breast_screening/config/jinja2_env.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@
66
from jinja2 import ChoiceLoader, Environment, PackageLoader
77
from markupsafe import Markup, escape
88

9-
from ..utils.date_formatting import (
10-
format_date,
11-
format_date_time,
12-
format_relative_date,
13-
format_time,
14-
format_time_range,
15-
)
16-
179

1810
def no_wrap(value):
1911
"""
@@ -54,10 +46,7 @@ def environment(**options):
5446
env.globals.update(
5547
{"static": static, "url": reverse, "STATIC_URL": settings.STATIC_URL}
5648
)
49+
env.filters["no_wrap"] = no_wrap
5750
env.filters["as_hint"] = as_hint
5851

59-
# TODO: format all dates and times in the presenter layer
60-
env.filters["format_date"] = format_date
61-
env.filters["format_time_range"] = format_time_range
62-
6352
return env

manage_breast_screening/record_a_mammogram/presenters.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,11 @@
44

55
from ..clinics.models import Appointment
66
from ..utils.date_formatting import format_date, format_relative_date, format_time
7+
from ..utils.string_formatting import format_age, format_nhs_number, sentence_case
78

89
Status = Appointment.Status
910

1011

11-
def sentence_case(value):
12-
"""
13-
Capitalise the first letter of a sentence.
14-
15-
>>> sentence_case('a quick brown fox jumps over the lazy dog')
16-
'A quick brown fox jumps over the lazy dog'
17-
18-
Unlike the built in `capitalize` filter, this will preserve
19-
capital letters already in the string:
20-
21-
>>> sentence_case('not in PACS')
22-
'Not in PACS'
23-
"""
24-
if not value:
25-
return ""
26-
27-
return value[0].upper() + value[1:]
28-
29-
30-
def format_nhs_number(value):
31-
"""
32-
Format an NHS number with spaces
33-
34-
>>> format_nhs_number('9998887777')
35-
'999 888 7777'
36-
"""
37-
if not value:
38-
return ""
39-
40-
digits = re.sub(r"\s", "", value)
41-
42-
return f"{digits[:3]} {digits[3:6]} {digits[6:]}"
43-
44-
45-
def format_age(value: int) -> str:
46-
return f"{value} years old"
47-
48-
4912
def status_colour(status):
5013
"""
5114
Color to render the status tag
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{% macro app_secondary_navigation(visually_hidden_title, items) %}
1+
{% macro appSecondaryNavigation(params) %}
22
{%- include "_components/secondary-navigation/template.jinja" -%}
33
{% endmacro %}

manage_breast_screening/templates/clinics/index.html

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,32 @@
44
{% from '_components/count/macro.jinja' import appCount %}
55
{% from '_components/secondary-navigation/macro.jinja' import appSecondaryNavigation %}
66

7-
{% set pageHeading %}
8-
{% if filter == 'today' %}
9-
Today’s clinics
10-
{% elif filter == 'upcoming' %}
11-
Upcoming clinics
12-
{% elif filter == 'completed' %}
13-
Completed clinics this week
14-
{% else %}
15-
All clinics this week
16-
{% endif %}
17-
{% endset %}
18-
197
{% block content %}
20-
<h1>{{pageHeading}}</h1>
8+
<h1>{{ presenter.heading }}</h1>
219

2210
{% set ns = namespace() %}
2311
{% set ns.secondaryNavItems = [] %}
2412

2513
{% for item in [
26-
{ "id": 'today', "label": 'Today' },
27-
{ "id": 'upcoming', "label": 'Upcoming' },
28-
{ "id": 'completed', "label": 'Completed' },
29-
{ "id": 'all', "label": 'All' }
14+
{ "id": 'today', "label": 'Today' },
15+
{ "id": 'upcoming', "label": 'Upcoming' },
16+
{ "id": 'completed', "label": 'Completed' },
17+
{ "id": 'all', "label": 'All' }
3018
] %}
3119
{% set href %}/clinics/{{ item.id }}{% endset %}
3220
{% set ns.secondaryNavItems = ns.secondaryNavItems + [{
33-
"text": (item.label + " " + appCount(filteredClinicCounts[item.id])) | safe,
34-
"href": href | trim,
35-
"current": true if item.id == filter
21+
"text": (item.label + " " + appCount(presenter.counts_by_filter[item.id])) | safe,
22+
"href": href | trim,
23+
"current": true if item.id == presenter.filter
3624
}] %}
3725
{% endfor %}
3826

3927
{{ appSecondaryNavigation({
40-
"visuallyHiddenTitle": "Secondary menu",
41-
"items": ns.secondaryNavItems
28+
"visuallyHiddenTitle": "Secondary menu",
29+
"items": ns.secondaryNavItems
4230
}) }}
4331

44-
{% if filteredClinics | length == 0 %}
32+
{% if presenter.clinics | length == 0 %}
4533
<p>No clinics found.</p>
4634
{% else %}
4735
<table class="nhsuk-table">
@@ -55,39 +43,31 @@ <h1>{{pageHeading}}</h1>
5543
</tr>
5644
</thead>
5745
<tbody class="nhsuk-table__body">
58-
{% for clinic in filteredClinics | sort(false, false, 'starts_at') %}
59-
{% set location = clinic.setting %}
60-
{% set events = clinic.slots %}
46+
{% for clinic in presenter.clinics %}
6147
<tr>
6248
<td>
6349
<a href="/clinics/{{ clinic.id }}" class="nhsuk-link">
64-
{#- FIXME-#}
65-
{% if location.type == 'mobile_unit' %}
66-
{{ location.name }} at {{ clinic.siteName }}
67-
{% else %}
68-
{{ location.name }}
69-
{% endif %}
50+
{{ clinic.location_name }}
7051
<br>
71-
({{ clinic.session_type() | capitalize }})
52+
({{ clinic.session_type }})
7253
</a>
7354
</td>
74-
<td>{{ clinic.starts_at | format_date | noWrap }}<br>
75-
{{clinic.time_range() | format_time_range | as_hint }}
55+
<td>{{ clinic.starts_at | no_wrap }}<br>
56+
{{clinic.time_range | as_hint }}
7657
</td>
7758
<td>
78-
{{ clinic.get_type_display() | capitalize }}
59+
{{ clinic.type }}
7960
<br>
80-
<span class="app-text-grey">{{ clinic.get_risk_type_display() | capitalize }}</span>
81-
61+
<span class="app-text-grey">{{ clinic.risk_type }}</span>
8262
</td>
8363

8464
<td class="nhsuk-table__cell--numeric">
85-
{{ events | length }}
65+
{{ clinic.number_of_slots }}
8666
</td>
87-
<td class="nhsuk-table__cell--numeric">
67+
<td>
8868
{{ tag({
89-
"html": clinic.get_state_display() | noWrap,
90-
"classes": "nhsuk-tag--" + STATUS_COLORS[clinic.state]
69+
"html": clinic.state.text | no_wrap,
70+
"classes": clinic.state.classes
9171
})}}
9272
</td>
9373
</tr>

0 commit comments

Comments
 (0)