Skip to content
Open
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
8 changes: 8 additions & 0 deletions manage_breast_screening/clinics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from django.db import models
from django.db.models import OuterRef, Subquery

from manage_breast_screening.dicom.models import Case

from ..auth.models import Role
from ..core.models import BaseModel
from ..participants.models import Appointment, Participant
Expand All @@ -34,6 +36,12 @@ def participants(self):
screeningepisode__appointment__clinic_slot__clinic__setting__provider=self
).distinct()

@property
def image_reading_cases(self):
return Case.objects.filter(
study__appointment__clinic_slot__clinic__setting__provider=self
)

def get_config(self) -> "ProviderConfig":
config, _ = ProviderConfig.objects.get_or_create(provider=self)
return config
Expand Down
2 changes: 2 additions & 0 deletions manage_breast_screening/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,5 @@ def list_env(key):
}

BYPASS_API_TOKEN_AUTH = boolean_env("BYPASS_API_TOKEN_AUTH", default=False)

READING_SESSION_DEFAULT_SIZE = 50
30 changes: 28 additions & 2 deletions manage_breast_screening/data/image_reading.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
studies:
- id: c6ff5cea-a5b8-4823-b307-e5faf587b474
date_and_time: '2026-04-27 09:00'
series:
- view_position: CC
laterality: R
Expand All @@ -18,6 +19,7 @@ studies:
images:
- image_file: set-02/lmlo.png
- id: 51a2f9a8-a8d4-4bcc-ad3f-7f20f4d71655
date_and_time: '2026-04-27 10:00'
series:
- view_position: CC
laterality: R
Expand All @@ -35,8 +37,8 @@ studies:
laterality: L
images:
- image_file: set-03/lmlo.png

- id: e468ecb0-33f2-4c27-a930-107c3fabd2d4
date_and_time: '2026-04-27 11:00'
series:
- view_position: CC
laterality: R
Expand All @@ -63,6 +65,7 @@ studies:
- image_file: image-library/repeat-large-b-lmlo.png
repeat_count: 0
- id: bedbcf0b-b9d8-4d78-960f-ede6c05563d7
date_and_time: '2026-04-27 12:00'
series:
- view_position: CC
laterality: R
Expand All @@ -81,6 +84,7 @@ studies:
images:
- image_file: set-04/lmlo.png
- id: 378f42d7-3361-43b8-bae6-0ce9113cba80
date_and_time: '2026-04-27 13:00'
series:
- view_position: CC
laterality: R
Expand All @@ -99,6 +103,7 @@ studies:
images:
- image_file: set-05/lmlo.png
- id: 15564f66-1d9d-4acd-b78f-1a32b4685124
date_and_time: '2026-04-27 14:00'
series:
- view_position: CC
laterality: R
Expand All @@ -117,6 +122,7 @@ studies:
images:
- image_file: set-06/lmlo.png
- id: 7dffd1de-88ae-4ac3-93d7-a8d4d0820e6b
date_and_time: '2026-04-27 15:00'
series:
- view_position: CC
laterality: R
Expand All @@ -142,8 +148,8 @@ studies:
repeat_count: 1
repeat_reasons:
- POSITIONING_ERROR

- id: 3a447fe4-f4b3-4961-a83c-5bab17a2cda7
date_and_time: '2026-04-27 16:00'
series:
- view_position: CC
laterality: R
Expand All @@ -162,6 +168,7 @@ studies:
images:
- image_file: set-07/lmlo.png
- id: 8fc7d736-f2d9-45ad-93cc-f289da5b720b
date_and_time: '2026-04-27 17:00'
series:
- view_position: CC
laterality: R
Expand All @@ -180,6 +187,7 @@ studies:
images:
- image_file: set-08/lmlo.png
- id: f61faccf-d9ea-4a9b-a284-021680565523
date_and_time: '2026-04-27 18:00'
series:
- view_position: CC
laterality: R
Expand All @@ -198,6 +206,7 @@ studies:
images:
- image_file: set-09/lmlo.png
- id: c089a4e6-8f66-4148-a14c-ec039e831c44
date_and_time: '2026-04-27 19:00'
series:
- view_position: CC
laterality: R
Expand All @@ -216,6 +225,7 @@ studies:
images:
- image_file: set-10/lmlo.png
- id: 3b25ff17-47b3-417c-9a8a-340c657f6578
date_and_time: '2026-04-27 20:00'
series:
- view_position: CC
laterality: R
Expand All @@ -234,6 +244,7 @@ studies:
images:
- image_file: set-11/lmlo.png
- id: 779b425a-b733-4fa9-89d5-fddbb58f09ea
date_and_time: '2026-04-27 21:00'
series:
- view_position: CC
laterality: R
Expand All @@ -252,6 +263,7 @@ studies:
images:
- image_file: set-12/lmlo.png
- id: 0378ff58-baa5-4484-8afb-258ebfd1e5ef
date_and_time: '2026-04-27 22:00'
series:
- view_position: CC
laterality: R
Expand All @@ -270,6 +282,7 @@ studies:
images:
- image_file: set-13/lmlo.png
- id: bb2ba1d3-1f72-428a-9c5a-39b50a5d9353
date_and_time: '2026-04-27 23:00'
series:
- view_position: CC
laterality: R
Expand All @@ -288,6 +301,7 @@ studies:
images:
- image_file: set-14/lmlo.png
- id: f0c600bf-dab2-44d5-aa5e-3b6481deeb48
date_and_time: '2026-04-28 00:00'
series:
- view_position: CC
laterality: R
Expand All @@ -306,6 +320,7 @@ studies:
images:
- image_file: set-15/lmlo.png
- id: bb2187a1-cd82-440f-94b1-46cef5938a23
date_and_time: '2026-04-28 01:00'
series:
- view_position: CC
laterality: R
Expand All @@ -324,6 +339,7 @@ studies:
images:
- image_file: set-16/lmlo.png
- id: 9ce6e923-aa30-4118-ac64-725f0fc37698
date_and_time: '2026-04-28 02:00'
series:
- view_position: CC
laterality: R
Expand All @@ -342,6 +358,7 @@ studies:
images:
- image_file: set-17/lmlo.png
- id: 9162a72b-3506-4b33-9acc-ef71bce07cb6
date_and_time: '2026-04-28 03:00'
series:
- view_position: CC
laterality: R
Expand All @@ -360,6 +377,7 @@ studies:
images:
- image_file: set-18/lmlo.png
- id: 72233799-7179-435f-be6d-e493a8c6ce78
date_and_time: '2026-04-28 04:00'
series:
- view_position: CC
laterality: R
Expand All @@ -378,6 +396,7 @@ studies:
images:
- image_file: set-19/lmlo.png
- id: 813a60ef-940c-4d39-bc5b-0167b2dc214c
date_and_time: '2026-04-28 05:00'
series:
- view_position: CC
laterality: R
Expand All @@ -396,6 +415,7 @@ studies:
images:
- image_file: set-20/lmlo.png
- id: 716f887d-8083-4cc8-9fea-d32213ca8fff
date_and_time: '2026-04-28 06:00'
series:
- view_position: CC
laterality: R
Expand All @@ -414,6 +434,7 @@ studies:
images:
- image_file: set-21/lmlo.png
- id: 4469102f-b8c6-4cba-9ed7-d77ee4f0ced8
date_and_time: '2026-04-28 07:00'
series:
- view_position: CC
laterality: R
Expand All @@ -432,6 +453,7 @@ studies:
images:
- image_file: set-22/lmlo.png
- id: 1ecd140e-c427-4c6d-bf20-d138610b3c0b
date_and_time: '2026-04-28 08:00'
series:
- view_position: CC
laterality: R
Expand All @@ -450,6 +472,7 @@ studies:
images:
- image_file: set-23/lmlo.png
- id: 4ba01e6b-2c5f-4373-9740-4fa6ca22ca1f
date_and_time: '2026-04-28 09:00'
series:
- view_position: CC
laterality: R
Expand All @@ -468,6 +491,7 @@ studies:
images:
- image_file: set-24/lmlo.png
- id: f082c62a-25b4-4aef-8fdf-415f31d296a4
date_and_time: '2026-04-28 10:00'
series:
- view_position: CC
laterality: R
Expand All @@ -486,6 +510,7 @@ studies:
images:
- image_file: set-25/lmlo.png
- id: d7d2c799-53fe-46c8-a81c-1cfb0e5fd4af
date_and_time: '2026-04-28 11:00'
series:
- view_position: CC
laterality: R
Expand All @@ -504,6 +529,7 @@ studies:
images:
- image_file: set-26/lmlo.png
- id: ff3fdaa4-9b7d-4d24-b91a-36e45d4c7dfb
date_and_time: '2026-04-28 12:00'
series:
- view_position: CC
laterality: R
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 6.0.4 on 2026-04-30 12:59

import uuid

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


class Migration(migrations.Migration):

dependencies = [
('dicom', '0009_rename_order_readingsessionitem_reading_order_and_more'),
]

operations = [
migrations.CreateModel(
name='Case',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cases', to='dicom.study')),
],
options={
'abstract': False,
},
),
migrations.RunSQL(
sql='TRUNCATE TABLE dicom_readingsession CASCADE',
reverse_sql='TRUNCATE table dicom_readingsession CASCADE',
),
migrations.AlterField(
model_name='reading',
name='study',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='readings', to='dicom.study'),
),
migrations.RemoveField(
model_name='readingsessionitem',
name='reading',
),
migrations.RemoveField(
model_name='readingsessionitem',
name='study',
),
migrations.AddField(
model_name='readingsessionitem',
name='case',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='reading_session_item', to='dicom.case'),
preserve_default=False,
)
]
2 changes: 1 addition & 1 deletion manage_breast_screening/dicom/migrations/max_migration.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0009_rename_order_readingsessionitem_reading_order_and_more
0010_auto_20260430_1359
51 changes: 39 additions & 12 deletions manage_breast_screening/dicom/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.postgres.fields import ArrayField
from django.core.files.storage import storages
from django.db import models
from django.db.models import Exists, OuterRef

from manage_breast_screening.core.models import BaseModel
from manage_breast_screening.manual_images.models import (
Expand Down Expand Up @@ -162,10 +163,10 @@ class BreastOpinions(models.TextChoices):

class Reading(BaseModel):
"""
One reader's opinion of a study. All of the opinions feed into the consensus read.
One reader's opinion of a study.
"""

study = models.ForeignKey(Study, on_delete=models.PROTECT, related_name="opinions")
study = models.ForeignKey(Study, on_delete=models.PROTECT, related_name="readings")
reader = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="readings"
)
Expand Down Expand Up @@ -217,9 +218,37 @@ class RecallForAssessmentDetails(BaseModel):
left_breast_comment = models.CharField(null=False, blank=True, default="")


class CaseQueryset(models.QuerySet):
def unassigned(self):
return self.filter(
~Exists(ReadingSessionItem.objects.filter(case=OuterRef("id")))
)

def where_same_study_has_not_been_assigned_to_reader(self, reader):
return self.filter(
~Exists(
ReadingSessionItem.objects.filter(
case__study=OuterRef("study_id"),
session__reader=reader,
)
)
)


class Case(BaseModel):
"""
A first or second read that needs doing for a study. These form the queue of cases
that can be picked up by an image reader.
"""

objects = CaseQueryset.as_manager()

study = models.ForeignKey(Study, on_delete=models.CASCADE, related_name="cases")
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.

We're using on_delete=models.CASCADE here but on_delete=models.PROTECT on Reading.

Just mentioning in case that wasn't intentional.



class ReadingSession(BaseModel):
"""
A grouping of studies that are read by a reader in a single session
A grouping of cases to be read by a reader in a single session
"""

reader = models.ForeignKey(
Expand All @@ -232,22 +261,20 @@ class ReadingSession(BaseModel):

class ReadingSessionItem(BaseModel):
"""
Assigns a study to a particular reading session, with an ordering.
An assignment of a pending reading to a reading session
"""

session = models.ForeignKey(
ReadingSession, on_delete=models.CASCADE, related_name="items"
)
study = models.ForeignKey(
Study, on_delete=models.PROTECT, related_name="reading_session_items"
case = models.OneToOneField(
Case, on_delete=models.PROTECT, related_name="reading_session_item"
)
reading_order = models.IntegerField()
reading = models.OneToOneField(
Reading,
on_delete=models.PROTECT,
related_name="reading_session_item",
null=True,
)

class Meta:
unique_together = [("session", "reading_order")]

@property
def study(self):
return self.case.study
Loading
Loading