Skip to content

Commit 4972068

Browse files
committed
Add models for image reads
A reading session has many reads. We will incrementally add reads as the session progresses rather than assigning them all at the beginning. The read record associates the session with a particular study, which contains a set of images taken during an appointment. We are going to allow a study to have 0-N reads rather than fix it at 2 at the model level. It's possible we might need to link additional studies to the read if the participant has a technical recall, but we'll worry about that later. I haven't modelled completion of a read, i.e. recording a decision and notes. There is also a separate piece around arbitration and assigning a final decision, which we'll model later.
1 parent bf2013f commit 4972068

3 files changed

Lines changed: 193 additions & 1 deletion

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Generated by Django 6.0.4 on 2026-04-23 13:37
2+
3+
import django.db.models.deletion
4+
import uuid
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('dicom', '0007_series_repeat_count_series_repeat_reasons_and_more'),
13+
('users', '__first__'),
14+
('users', '__first__'),
15+
('users', '__first__'),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name='ReadingSession',
21+
fields=[
22+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
23+
('created_at', models.DateTimeField(auto_now_add=True)),
24+
('updated_at', models.DateTimeField(auto_now=True)),
25+
('session_size', models.IntegerField()),
26+
('reader', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
27+
],
28+
options={
29+
'abstract': False,
30+
},
31+
),
32+
migrations.CreateModel(
33+
name='Reading',
34+
fields=[
35+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
36+
('created_at', models.DateTimeField(auto_now_add=True)),
37+
('updated_at', models.DateTimeField(auto_now=True)),
38+
('opinion', models.CharField(choices=[('NORMAL', 'Normal'), ('TECHNICAL_RECALL', 'Technical recall'), ('RECALL', 'Recall for assessment')])),
39+
('additional_details', models.TextField(blank=True, default='')),
40+
('reader', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
41+
('study', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='opinions', to='dicom.study')),
42+
],
43+
options={
44+
'unique_together': {('study', 'reader')},
45+
},
46+
),
47+
migrations.CreateModel(
48+
name='RecallForAssessmentDetails',
49+
fields=[
50+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
51+
('created_at', models.DateTimeField(auto_now_add=True)),
52+
('updated_at', models.DateTimeField(auto_now=True)),
53+
('right_breast_opinion', models.CharField(choices=[('NORMAL', 'Normal'), ('ABNORMAL', 'Abnormal, recall for assessment')])),
54+
('right_breast_comment', models.CharField(blank=True, default='')),
55+
('left_breast_opinion', models.CharField(choices=[('NORMAL', 'Normal'), ('ABNORMAL', 'Abnormal, recall for assessment')])),
56+
('left_breast_comment', models.CharField(blank=True, default='')),
57+
('reading', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='recall_for_assessment_details', to='dicom.reading')),
58+
],
59+
options={
60+
'abstract': False,
61+
},
62+
),
63+
migrations.CreateModel(
64+
name='RetakeRequest',
65+
fields=[
66+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
67+
('created_at', models.DateTimeField(auto_now_add=True)),
68+
('updated_at', models.DateTimeField(auto_now=True)),
69+
('reading', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='retake_requests', to='dicom.reading')),
70+
('laterality', models.CharField(choices=[('L', 'L'), ('R', 'R')])),
71+
('view_position', models.CharField(choices=[('CC', 'Cc'), ('MLO', 'Mlo')])),
72+
],
73+
options={
74+
'unique_together': {('reading', 'view_position', 'laterality')},
75+
},
76+
),
77+
migrations.CreateModel(
78+
name='ReadingSessionItem',
79+
fields=[
80+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
81+
('created_at', models.DateTimeField(auto_now_add=True)),
82+
('updated_at', models.DateTimeField(auto_now=True)),
83+
('order', models.IntegerField()),
84+
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='dicom.readingsession')),
85+
('study', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reading_session_items', to='dicom.study')),
86+
('reading', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reading_session_item', to='dicom.reading')),
87+
],
88+
options={
89+
'unique_together': {('session', 'order')},
90+
},
91+
),
92+
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0007_series_repeat_count_series_repeat_reasons_and_more
1+
0008_readingsession_reading_recallforassessmentdetails_and_more_updated

manage_breast_screening/dicom/models.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import uuid
22

3+
from django.conf import settings
34
from django.contrib.postgres.fields import ArrayField
45
from django.core.files.storage import storages
56
from django.db import models
67

8+
from manage_breast_screening.core.models import BaseModel
79
from manage_breast_screening.manual_images.models import (
810
IncompleteImagesReason,
911
RepeatReason,
@@ -146,3 +148,101 @@ def laterality_and_view(self):
146148

147149
def __str__(self):
148150
return self.laterality_and_view
151+
152+
153+
class Opinions(models.TextChoices):
154+
NORMAL = "NORMAL", "Normal"
155+
TECHNICAL_RECALL = "TECHNICAL_RECALL", "Technical recall"
156+
RECALL = "RECALL", "Recall for assessment"
157+
158+
159+
class BreastOpinions(models.TextChoices):
160+
NORMAL = "NORMAL", "Normal"
161+
ABNORMAL = "ABNORMAL", "Abnormal, recall for assessment"
162+
163+
164+
class Reading(BaseModel):
165+
"""
166+
One reader's opinion of a study. All of the opinions feed into the consensus read.
167+
"""
168+
169+
study = models.ForeignKey(Study, on_delete=models.PROTECT, related_name="opinions")
170+
reader = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
171+
opinion = models.CharField(choices=Opinions)
172+
additional_details = models.TextField(null=False, blank=True, default="")
173+
174+
class Meta:
175+
unique_together = [("study", "reader")]
176+
177+
178+
class ViewPositions(models.TextChoices):
179+
CC = "CC"
180+
MLO = "MLO"
181+
182+
183+
class Laterality(models.TextChoices):
184+
L = "L"
185+
R = "R"
186+
187+
188+
class RetakeRequest(BaseModel):
189+
"""
190+
Indicates the views that need retaking, in the case of a technical recall opinion
191+
"""
192+
193+
reading = models.ForeignKey(
194+
Reading, on_delete=models.PROTECT, related_name="retake_requests"
195+
)
196+
view_position = models.CharField(choices=ViewPositions)
197+
laterality = models.CharField(choices=Laterality)
198+
199+
class Meta:
200+
unique_together = [("reading", "view_position", "laterality")]
201+
202+
203+
class RecallForAssessmentDetails(BaseModel):
204+
"""
205+
Further details of a recall for assessment opinion
206+
"""
207+
208+
reading = models.OneToOneField(
209+
Reading,
210+
on_delete=models.PROTECT,
211+
related_name="recall_for_assessment_details",
212+
)
213+
right_breast_opinion = models.CharField(choices=BreastOpinions)
214+
right_breast_comment = models.CharField(null=False, blank=True, default="")
215+
left_breast_opinion = models.CharField(choices=BreastOpinions)
216+
left_breast_comment = models.CharField(null=False, blank=True, default="")
217+
218+
219+
class ReadingSession(BaseModel):
220+
"""
221+
A grouping of studies that are read by a reader in a single session
222+
"""
223+
224+
reader = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
225+
session_size = models.IntegerField()
226+
227+
228+
class ReadingSessionItem(BaseModel):
229+
"""
230+
Assigns a study to a particular reading session, with an ordering.
231+
"""
232+
233+
session = models.ForeignKey(
234+
ReadingSession, on_delete=models.CASCADE, related_name="items"
235+
)
236+
study = models.ForeignKey(
237+
Study, on_delete=models.PROTECT, related_name="reading_session_items"
238+
)
239+
order = models.IntegerField()
240+
reading = models.OneToOneField(
241+
Reading,
242+
on_delete=models.PROTECT,
243+
related_name="reading_session_item",
244+
null=True,
245+
)
246+
247+
class Meta:
248+
unique_together = [("session", "order")]

0 commit comments

Comments
 (0)