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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Database migrations are handled by [Django's database migration functionality](h
- `poetry run manage.py migrate` loads database migrations
- `poetry run manage.py makemigrations` generates new database migrations

Note the database migration runs in the deployment pipeline *after* the application deployment. The deployed code must be compatible with the schema before AND after the schema changes. This also removes potential errors when using a rolling app deployment as multiple app versions may access the database at the same time. To enforce it, make sure to always separate code changes and database migrations into different pull requests.
Note the database migration runs in the deployment pipeline _after_ the application deployment. The deployed code must be compatible with the schema before AND after the schema changes. This also removes potential errors when using a rolling app deployment as multiple app versions may access the database at the same time. To enforce it, make sure to always separate code changes and database migrations into different pull requests.

### Django admin

Expand Down
2 changes: 1 addition & 1 deletion manage_breast_screening/assets/sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ a[href="#"] {
top: 10px;
right: 0;
text-align: right;
}
}
12 changes: 10 additions & 2 deletions manage_breast_screening/clinics/fixtures/clinics.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
"starts_at": "2025-04-01T09:00:00Z",
"ends_at": "2025-04-04T17:00:00Z",
"type": "SCREENING",
"risk_type": "ROUTINE_RISK",
"state": "IN_PROGRESS"
"risk_type": "ROUTINE_RISK"
}
},
{
Expand Down Expand Up @@ -86,5 +85,14 @@
"starts_at": "2025-05-01T13:00:00Z",
"duration_in_minutes": 30
}
},
{
"model": "clinics.clinicstatus",
"pk": "82da38f1-f39f-4692-8a76-d621630caa69",
"fields": {
"created_at": "2025-06-20T09:22:36.070Z",
"state": "IN_PROGRESS",
"clinic": "ce662c8b-92dc-4c82-9d5d-1ddcb201e2b6"
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Generated by Django 5.2.3 on 2025-06-17 12:51

import uuid

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


def extract_status(apps, schema_editor):
Clinic = apps.get_model("clinics", "Clinic")
for clinic in Clinic.objects.all():
clinic.statuses.create(state=clinic.state)


def revert_extract_status(apps, schema_editor):
Clinic = apps.get_model("clinics", "Clinic")
for clinic in Clinic.objects.all():
clinic.state = clinic.statuses.first().state
clinic.save()


class Migration(migrations.Migration):

dependencies = [
("clinics", "0012_remove_screeningepisode_participant_and_more"),
]

operations = [
migrations.AlterField(
model_name="clinic",
name="state",
field=models.CharField(
choices=[
("SCHEDULED", "Scheduled"),
("IN_PROGRESS", "In progress"),
("CLOSED", "Closed"),
("CANCELLED", "Cancelled"),
],
max_length=50,
null=True,
),
),
migrations.CreateModel(
name="ClinicStatus",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"state",
models.CharField(
choices=[
("SCHEDULED", "Scheduled"),
("IN_PROGRESS", "In progress"),
("CLOSED", "Closed"),
("CANCELLED", "Cancelled"),
],
max_length=50,
),
),
(
"clinic",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="statuses",
to="clinics.clinic",
),
),
],
options={
"ordering": ["-created_at"],
},
),
migrations.RunPython(extract_status, revert_extract_status),
migrations.RemoveField(
model_name="clinic",
name="state",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 5.2.3 on 2025-06-20 11:13

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('clinics', '0013_alter_clinic_setting_alter_clinicslot_clinic_and_more'),
('clinics', '0013_move_clinic_state_to_table'),
]

operations = [
]
45 changes: 31 additions & 14 deletions manage_breast_screening/clinics/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from datetime import date
from enum import StrEnum

Expand Down Expand Up @@ -61,14 +62,11 @@ def completed(self):
"""
return self.filter(starts_at__date__lt=date.today())

def with_statuses(self):
return self.prefetch_related("statuses")

class Clinic(BaseModel):
class State:
SCHEDULED = "SCHEDULED"
IN_PROGRESS = "IN_PROGRESS"
CLOSED = "CLOSED"
CANCELLED = "CANCELLED"

class Clinic(BaseModel):
class RiskType:
MIXED_RISK = "MIXED_RISK"
ROUTINE_RISK = "ROUTINE_RISK"
Expand All @@ -83,13 +81,6 @@ class TimeOfDay:
MORNING = "morning"
AFTERNOON = "afternoon"

STATE_CHOICES = {
State.SCHEDULED: "Scheduled",
State.IN_PROGRESS: "In progress",
State.CLOSED: "Closed",
State.CANCELLED: "Cancelled",
}

RISK_TYPE_CHOICES = {
RiskType.MIXED_RISK: "Mixed risk",
RiskType.ROUTINE_RISK: "Routine risk",
Expand All @@ -103,10 +94,12 @@ class TimeOfDay:
ends_at = models.DateTimeField()
type = models.CharField(choices=TYPE_CHOICES, max_length=50)
risk_type = models.CharField(choices=RISK_TYPE_CHOICES, max_length=50)
state = models.CharField(choices=STATE_CHOICES, max_length=50)

objects = ClinicQuerySet.as_manager()

def current_status(self):
return self.statuses.first()

def session_type(self):
start_hour = self.starts_at.hour
duration = (self.ends_at - self.starts_at).seconds
Expand Down Expand Up @@ -137,3 +130,27 @@ class ClinicSlot(BaseModel):
)
starts_at = models.DateTimeField()
duration_in_minutes = models.IntegerField()


class ClinicStatus(models.Model):
SCHEDULED = "SCHEDULED"
IN_PROGRESS = "IN_PROGRESS"
CLOSED = "CLOSED"
CANCELLED = "CANCELLED"

STATE_CHOICES = [
(SCHEDULED, "Scheduled"),
(IN_PROGRESS, "In progress"),
(CLOSED, "Closed"),
(CANCELLED, "Cancelled"),
]

class Meta:
ordering = ["-created_at"]

id = models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
state = models.CharField(choices=STATE_CHOICES, max_length=50)
clinic = models.ForeignKey(
Clinic, on_delete=models.PROTECT, related_name="statuses"
)
16 changes: 10 additions & 6 deletions manage_breast_screening/clinics/presenters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from ..core.utils.date_formatting import format_date, format_time_range
from ..core.utils.string_formatting import sentence_case
from .models import Clinic
from ..mammograms.presenters import AppointmentPresenter
from .models import ClinicStatus


class ClinicsPresenter:
Expand All @@ -26,9 +26,9 @@ def heading(self):

class ClinicPresenter:
STATUS_COLORS = {
Clinic.State.SCHEDULED: "blue", # default blue
Clinic.State.IN_PROGRESS: "blue",
Clinic.State.CLOSED: "grey",
ClinicStatus.SCHEDULED: "blue", # default blue
ClinicStatus.IN_PROGRESS: "blue",
ClinicStatus.CLOSED: "grey",
}

def __init__(self, clinic):
Expand All @@ -44,9 +44,13 @@ def __init__(self, clinic):

@property
def state(self):
status = self._clinic.current_status()
value = status.state
text = status.get_state_display()

return {
"text": self._clinic.get_state_display(),
"classes": "nhsuk-tag--" + self.STATUS_COLORS[self._clinic.state],
"text": text,
"classes": "nhsuk-tag--" + self.STATUS_COLORS[value],
}

@property
Expand Down
26 changes: 24 additions & 2 deletions manage_breast_screening/clinics/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timedelta, timezone

from factory.declarations import LazyAttribute, Sequence, SubFactory
from factory import post_generation
from factory.declarations import LazyAttribute, RelatedFactoryList, Sequence, SubFactory
from factory.django import DjangoModelFactory
from factory.fuzzy import FuzzyChoice

Expand All @@ -24,20 +25,41 @@ class Meta:
provider = SubFactory(ProviderFactory)


class ClinicStatusFactory(DjangoModelFactory):
class Meta:
model = models.ClinicStatus

clinic = None
state = models.ClinicStatus.SCHEDULED


class ClinicFactory(DjangoModelFactory):
class Meta:
model = models.Clinic
django_get_or_create = ("starts_at", "ends_at")

type = FuzzyChoice(models.Clinic.TYPE_CHOICES)
risk_type = FuzzyChoice(models.Clinic.RISK_TYPE_CHOICES)
state = models.Clinic.State.SCHEDULED
starts_at = Sequence(
lambda n: datetime(2025, 1, 1, 9, tzinfo=timezone.utc) + timedelta(hours=n)
)
ends_at = LazyAttribute(lambda o: o.starts_at + timedelta(minutes=30))
setting = SubFactory(SettingFactory)

# Create a status by default
statuses = RelatedFactoryList(
ClinicStatusFactory, size=1, factory_related_name="clinic"
)

# Allow passing an explicit status
# e.g. `current_status=ClinicStatus.IN_PROGRESS`
@post_generation
def current_status(obj, create, extracted, **kwargs):
if not create or not extracted:
return

obj.statuses.add(ClinicStatusFactory.create(state=extracted, clinic=obj))


class ClinicSlotFactory(DjangoModelFactory):
class Meta:
Expand Down
7 changes: 4 additions & 3 deletions manage_breast_screening/clinics/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
from .factories import ClinicFactory


def test_clinic_is_scheduled():
factory = ClinicFactory.build()
assert factory.state == models.Clinic.State.SCHEDULED
@pytest.mark.django_db
def test_clinic_current_status():
clinic = ClinicFactory.create(current_status=models.ClinicStatus.SCHEDULED)
assert clinic.current_status().state == models.ClinicStatus.SCHEDULED


@pytest.mark.django_db
Expand Down
17 changes: 10 additions & 7 deletions manage_breast_screening/clinics/tests/test_presenters.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import uuid
from datetime import datetime
from unittest.mock import MagicMock
import uuid
import pytest
Comment on lines +1 to -4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything we can do to fix import ordering automatically?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a tool called isort. Looks like we haven't enabled it yet but I can raise another PR...


import pytest
from django.urls import reverse

from manage_breast_screening.clinics.presenters import (
ClinicPresenter,
AppointmentListPresenter,
)
from manage_breast_screening.clinics.models import Clinic
from ..models import Clinic
from ..presenters import AppointmentListPresenter, ClinicPresenter
from .factories import ClinicStatusFactory


@pytest.fixture
Expand All @@ -26,6 +24,7 @@ def mock_clinic():
}
mock.get_type_display.return_value = "Screening"
mock.get_risk_type_display.return_value = "Routine"
mock.current_status.return_value = ClinicStatusFactory.build()

return mock

Expand All @@ -40,6 +39,10 @@ def test_clinic_presenter(mock_clinic):
assert presenter.time_range == "9am to 3pm"
assert presenter.type == "Screening"
assert presenter.risk_type == "Routine"
assert presenter.state == {
"classes": "nhsuk-tag--blue",
"text": "Scheduled",
}


class TestAppointmentListPresenter:
Expand Down
6 changes: 0 additions & 6 deletions manage_breast_screening/clinics/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
from .models import Clinic
from ..participants.models import Appointment

STATUS_COLORS = {
Clinic.State.SCHEDULED: "blue", # default blue
Clinic.State.IN_PROGRESS: "blue",
Clinic.State.CLOSED: "grey",
}


def clinic_list(request, filter="today"):
clinics = Clinic.objects.prefetch_related("setting").by_filter(filter)
Expand Down