Skip to content

Commit c8d25d1

Browse files
authored
Merge pull request #680 from jbernal0019/master
Implement the execute control for PACS queries
2 parents 3d56227 + 4411a3f commit c8d25d1

File tree

6 files changed

+152
-13
lines changed

6 files changed

+152
-13
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.9 on 2025-12-27 17:21
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('pacsfiles', '0006_alter_pacs_identifier'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='pacsquery',
15+
name='execute',
16+
field=models.BooleanField(blank=True, default=True),
17+
),
18+
]

chris_backend/pacsfiles/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class PACSQuery(models.Model):
5555
title = models.CharField(max_length=300, db_index=True)
5656
query = models.JSONField()
5757
description = models.CharField(max_length=700, blank=True)
58+
execute = models.BooleanField(blank=True, default=True)
5859
result = models.TextField(blank=True)
5960
status = models.CharField(max_length=10, choices=PACS_QUERY_STATUS_CHOICES,
6061
default='created')
@@ -108,8 +109,8 @@ class PACSQueryFilter(FilterSet):
108109
class Meta:
109110
model = PACSQuery
110111
fields = ['id', 'min_creation_date', 'max_creation_date', 'title_exact',
111-
'title', 'status', 'description', 'pacs_id', 'pacs_identifier',
112-
'owner_username']
112+
'title', 'status', 'execute', 'description', 'pacs_id',
113+
'pacs_identifier', 'owner_username']
113114

114115

115116
PACS_RETRIEVE_STATUS_CHOICES = PACS_QUERY_STATUS_CHOICES

chris_backend/pacsfiles/serializers.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
class PACSSerializer(serializers.HyperlinkedModelSerializer):
21+
active = serializers.BooleanField(required=False, default=True)
2122
folder_path = serializers.ReadOnlyField(source='folder.path')
2223
folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail',
2324
read_only=True)
@@ -32,17 +33,20 @@ class Meta:
3233

3334

3435
class PACSQuerySerializer(serializers.HyperlinkedModelSerializer):
36+
title = serializers.CharField(max_length=300, required=False)
3537
query = serializers.JSONField(binary=True, required=False)
3638
pacs_identifier = serializers.ReadOnlyField(source='pacs.identifier')
3739
owner_username = serializers.ReadOnlyField(source='owner.username')
3840
result = serializers.ReadOnlyField()
3941
status = serializers.ReadOnlyField()
42+
# explicitly set default to True for boolean fields
43+
execute = serializers.BooleanField(required=False, default=True)
4044
retrieve_list = serializers.HyperlinkedIdentityField(view_name='pacsretrieve-list')
4145

4246
class Meta:
4347
model = PACSQuery
4448
fields = ('url', 'id', 'creation_date', 'title', 'query', 'description',
45-
'status', 'pacs_identifier', 'owner_username', 'result',
49+
'status', 'pacs_identifier', 'owner_username', 'execute', 'result',
4650
'retrieve_list')
4751

4852
def __init__(self, *args, **kwargs):
@@ -83,11 +87,25 @@ def update(self, instance, validated_data):
8387
f'for pacs {pacs.identifier}')
8488
raise serializers.ValidationError([error_msg])
8589

90+
def validate_execute(self, execute):
91+
"""
92+
Overriden to validate a change of execute status.
93+
"""
94+
instance = self.instance
95+
if instance and instance.execute and not execute:
96+
msg = "Can not change field execute from true to false."
97+
raise serializers.ValidationError([msg])
98+
return execute
99+
86100
def validate(self, data):
87101
"""
88-
Overriden to validate that the query field is in data when creating a new query.
102+
Overriden to validate that required fields are in data when creating a new query.
89103
"""
90104
if not self.instance: # on create
105+
if 'title' not in data:
106+
raise serializers.ValidationError(
107+
{'title': ["This field is required."]})
108+
91109
if 'query' not in data:
92110
raise serializers.ValidationError(
93111
{'query': ["This field is required."]})

chris_backend/pacsfiles/tests/test_serializers.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
import logging
33
import io
4-
import os
54

65
from django.contrib.auth.models import User
76
from django.test import TestCase, tag
@@ -111,6 +110,37 @@ def test_update_failure_pacs_user_title_combination_already_exists(self):
111110
with self.assertRaises(serializers.ValidationError):
112111
pacs_query_serializer.update(pacs_query, data)
113112

113+
def test_validate_execute(self):
114+
"""
115+
Test whether overriden validate_execute method raises a serializers.ValidationError
116+
when the execute field changes from true to false.
117+
"""
118+
user = User.objects.get(username=self.username)
119+
pacs = PACS.objects.get(identifier=self.pacs_name)
120+
query = {'SeriesInstanceUID': '1.3.12.2.1107'}
121+
122+
pacs_query, _ = PACSQuery.objects.get_or_create(title='query5', query=query,
123+
owner=user, pacs=pacs)
124+
pacs_query_serializer = PACSQuerySerializer(pacs_query)
125+
126+
with self.assertRaises(serializers.ValidationError):
127+
pacs_query_serializer.validate_execute(False)
128+
129+
130+
def test_validate_validates_required_query_field_on_create(self):
131+
"""
132+
Test whether overriden validate method validates that the query field must
133+
be provided when creating a new PACS query.
134+
"""
135+
user = User.objects.get(username=self.username)
136+
pacs = PACS.objects.get(identifier=self.pacs_name)
137+
138+
data = {'title': 'query6', 'owner': user, 'pacs': pacs}
139+
pacs_query_serializer = PACSQuerySerializer(data=data)
140+
141+
with self.assertRaises(serializers.ValidationError):
142+
pacs_query_serializer.validate(data)
143+
114144

115145
class PACSSeriesSerializerTests(SerializerTests):
116146

chris_backend/pacsfiles/tests/test_views.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,13 @@ def setUp(self):
188188

189189
self.create_read_url = reverse("pacsquery-list", kwargs={"pk": pacs.id})
190190

191-
query = {'SeriesInstanceUID': '2.3.15.2.1057'}
192-
pacs_query, _ = PACSQuery.objects.get_or_create(title='query10', query=query,
191+
self.query = {'SeriesInstanceUID': '2.3.15.2.1057'}
192+
pacs_query, _ = PACSQuery.objects.get_or_create(title='query10', query=self.query,
193193
owner=user, pacs=pacs)
194194

195195
self.post = json.dumps(
196196
{"template": {"data": [{"name": "title", "value": 'test1'},
197-
{"name": "query", "value": json.dumps(query)}]}})
197+
{"name": "query", "value": json.dumps(self.query)}]}})
198198

199199
def test_pacs_query_list_success(self):
200200
self.client.login(username=self.username, password=self.password)
@@ -217,7 +217,7 @@ def test_pacs_query_list_failure_unauthenticated(self):
217217
response = self.client.get(self.create_read_url)
218218
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
219219

220-
def test_pacs_query_create_success(self):
220+
def test_pacs_query_create_success_execute(self):
221221
with mock.patch.object(views.send_pacs_query, 'delay',
222222
return_value=None) as delay_mock:
223223
# make API request
@@ -230,6 +230,25 @@ def test_pacs_query_create_success(self):
230230
delay_mock.assert_called_with(response.data['id'])
231231
self.assertEqual(response.data['status'], 'created')
232232

233+
def test_pacs_query_create_success_do_not_execute(self):
234+
235+
post = json.dumps(
236+
{"template": {"data": [{"name": "title", "value": 'test2'},
237+
{"name": "execute", "value": False},
238+
{"name": "query", "value": json.dumps(self.query)}]}})
239+
240+
with mock.patch.object(views.send_pacs_query, 'delay',
241+
return_value=None) as delay_mock:
242+
# make API request
243+
self.client.login(username=self.username, password=self.password)
244+
response = self.client.post(self.create_read_url, data=post,
245+
content_type=self.content_type)
246+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
247+
248+
# check that the send_pacs_query task was not called
249+
delay_mock.assert_not_called()
250+
self.assertEqual(response.data['status'], 'created')
251+
233252
def test_pacs_query_create_failure_unauthenticated(self):
234253
response = self.client.post(self.create_read_url, data=self.post,
235254
content_type=self.content_type)
@@ -358,6 +377,44 @@ def test_pacs_query_update_success(self):
358377
content_type=self.content_type)
359378
self.assertContains(response, "Test query")
360379

380+
def test_pacs_query_update_success_execute(self):
381+
pacs = PACS.objects.get(identifier=self.pacs_name)
382+
user = User.objects.get(username=self.username)
383+
query = {'SeriesInstanceUID': '2.3.15.2.1057'}
384+
385+
pacs_query, _ = PACSQuery.objects.get_or_create(title='query2', execute=False,
386+
query=query, owner=user, pacs=pacs)
387+
read_update_delete_url = reverse("pacsquery-detail", kwargs={"pk":pacs_query.id})
388+
389+
put = json.dumps({
390+
"template": {"data": [{"name": "title", "value": "Test query2"},
391+
{"name": "execute", "value": True}]}})
392+
393+
self.client.login(username=self.username, password=self.password)
394+
395+
with mock.patch.object(views.send_pacs_query, 'delay',
396+
return_value=None) as delay_mock:
397+
response = self.client.put(read_update_delete_url, data=put,
398+
content_type=self.content_type)
399+
# check that the send_pacs_query task was called with appropriate args
400+
delay_mock.assert_called_with(response.data['id'])
401+
self.assertContains(response, "Test query2")
402+
403+
def test_pacs_query_update_success_do_not_execute_again(self):
404+
put = json.dumps({
405+
"template": {"data": [{"name": "title", "value": "Test query"},
406+
{"name": "execute", "value": True}]}})
407+
408+
self.client.login(username=self.username, password=self.password)
409+
410+
with mock.patch.object(views.send_pacs_query, 'delay',
411+
return_value=None) as delay_mock:
412+
response = self.client.put(self.read_update_delete_url, data=put,
413+
content_type=self.content_type)
414+
# check that the send_pacs_query task was not called
415+
delay_mock.assert_not_called()
416+
self.assertContains(response, "Test query")
417+
361418
def test_pacs_query_update_failure_unauthenticated(self):
362419
response = self.client.put(self.read_update_delete_url, data=self.put,
363420
content_type=self.content_type)

chris_backend/pacsfiles/views.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def list(self, request, *args, **kwargs):
156156
response = services.append_collection_links(response, links)
157157

158158
# append write template
159-
template_data = {'title': '', 'query': '', 'description': ''}
159+
template_data = {'title': '', 'query': '', 'description': '', 'execute': ''}
160160
return services.append_collection_template(response, template_data)
161161

162162
def get_pacs_queries_queryset(self):
@@ -175,11 +175,13 @@ def get_pacs_queries_queryset(self):
175175
def perform_create(self, serializer):
176176
"""
177177
Overriden to associate the owner and the pacs with the PACS query before first
178-
saving to the DB. Then the PACS query operation is sent to the remote PACS.
178+
saving to the DB. Then the PACS query operation is sent to the remote PACS if
179+
the execute field is set to True.
179180
"""
180181
pacs = self.get_object()
181182
pacs_query = serializer.save(owner=self.request.user, pacs=pacs)
182-
send_pacs_query.delay(pacs_query.id) # call async task
183+
if pacs_query.execute:
184+
send_pacs_query.delay(pacs_query.id) # call async task
183185

184186

185187
class AllPACSQueryList(generics.ListAPIView):
@@ -249,9 +251,22 @@ def retrieve(self, request, *args, **kwargs):
249251
Overriden to append a collection+json template to the response.
250252
"""
251253
response = super(PACSQueryDetail, self).retrieve(request, *args, **kwargs)
252-
template_data = {'title': '', 'description': ''}
254+
template_data = {'title': '', 'description': '', 'execute': ''}
253255
return services.append_collection_template(response, template_data)
254256

257+
def perform_update(self, serializer):
258+
"""
259+
Overriden to execute the PACS query if the execute field changes from False
260+
to True.
261+
"""
262+
if 'execute' in self.request.data:
263+
instance = self.get_object()
264+
265+
if self.request.data['execute'] and not instance.execute:
266+
send_pacs_query.delay(instance.id) # call async task
267+
268+
super(PACSQueryDetail, self).perform_update(serializer)
269+
255270

256271
class PACSRetrieveList(generics.ListCreateAPIView):
257272
"""

0 commit comments

Comments
 (0)