-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathwidgets.py
More file actions
306 lines (261 loc) · 10.1 KB
/
widgets.py
File metadata and controls
306 lines (261 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
from typing import NamedTuple
from uuid import UUID
from django.core.exceptions import ValidationError
from django.db.models import QuerySet, TextChoices
from django.forms import (
CharField,
HiddenInput,
ModelChoiceField,
ModelMultipleChoiceField,
MultiValueField,
MultiWidget,
)
from django.forms.widgets import ChoiceWidget, TextInput
from grandchallenge.cases.models import Image
from grandchallenge.components.models import ComponentInterfaceValue
from grandchallenge.core.guardian import (
filter_by_permission,
get_object_if_allowed,
)
from grandchallenge.uploads.models import UserUpload
from grandchallenge.uploads.widgets import (
DICOMUserUploadMultipleWidget,
UserUploadMultipleWidget,
)
class ImageWidgetChoices(TextChoices):
IMAGE_SEARCH = "IMAGE_SEARCH"
IMAGE_UPLOAD = "IMAGE_UPLOAD"
IMAGE_SELECTED = "IMAGE_SELECTED"
UNDEFINED = "UNDEFINED"
class ImageSearchWidget(ChoiceWidget, HiddenInput):
template_name = "cases/image_search_widget.html"
input_type = None
name = None
def __init__(self, *args, name=None, **kwargs):
super().__init__(*args, **kwargs)
if name:
self.name = name
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
if self.name:
context["widget"]["name"] = self.name
return context
class FlexibleImageWidget(MultiWidget):
template_name = "cases/flexible_image_widget.html"
def __init__(
self,
attrs=None,
):
widgets = (
ImageSearchWidget(),
UserUploadMultipleWidget(),
)
super().__init__(widgets, attrs)
def decompress(self, value): # noqa: C901
if not value:
return [None, None]
if isinstance(value, (list, tuple)):
if len(value) == 1:
item = value[0]
if item == "":
return [None, None]
if not item:
raise RuntimeError("Unexpected value")
if item in ImageWidgetChoices.names:
return [None, None]
if Image.objects.filter(pk=item).exists():
return [str(item), None]
# can also just be a single UserUpload
return [None, value]
else:
# can be a list of UserUploads
return [None, value]
if isinstance(value, UUID):
# when an image or user upload is preselected as current_value
if Image.objects.filter(pk=value).exists():
return [value, None]
elif UserUpload.objects.filter(pk=value).exists():
return [None, [value]]
else:
return [None, None]
raise RuntimeError("Unrecognized value type")
def value_from_datadict(self, data, files, name):
try:
value = data.getlist(name)
except AttributeError:
value = data.get(name)
return self.decompress(value)
class FlexibleImageField(MultiValueField):
widget = FlexibleImageWidget
def __init__( # noqa C901
self,
*args,
user=None,
initial=None,
**kwargs,
):
image_search_queryset = filter_by_permission(
queryset=Image.objects.all(),
user=user,
codename="view_image",
)
upload_queryset = filter_by_permission(
queryset=UserUpload.objects.all(),
user=user,
codename="change_userupload",
).filter(status=UserUpload.StatusChoices.COMPLETED)
list_fields = [
ModelChoiceField(queryset=image_search_queryset, required=False),
ModelMultipleChoiceField(queryset=upload_queryset, required=False),
]
# The `current_value` is added to the widget attrs to display an appropriate
# title in the initial dropdown and to add the pk(s) as hidden input
# in the widget template.
self.current_value = None
if initial:
if isinstance(initial, ComponentInterfaceValue):
# This can happen on display set or archive item update forms,
# the value is then taken from the model instance
# unless the value is in the form data.
if user.has_perm("view_image", initial.image):
self.current_value = [initial.image]
initial = initial.image.pk
else:
initial = None
elif isinstance(initial, (list, tuple)):
# It can be a list of UserUpload objects
uploads = []
for i in initial:
if isinstance(i, UserUpload):
uploads.append(i)
else:
if upload := get_object_if_allowed(
model=UserUpload,
pk=i,
user=user,
codename="change_userupload",
):
uploads.append(upload)
if len(uploads) != 0:
self.current_value = uploads
# Otherwise the value is taken from the form data and will always take
# the form of a pk for either an Image object or a UserUpload object.
elif image := get_object_if_allowed(
model=Image, pk=initial, user=user, codename="view_image"
):
self.current_value = [image]
elif upload := get_object_if_allowed(
model=UserUpload,
pk=initial,
user=user,
codename="change_userupload",
):
self.current_value = [upload]
else:
initial = None
super().__init__(
*args,
fields=list_fields,
initial=initial,
require_all_fields=False,
**kwargs,
)
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
attrs["current_value"] = self.current_value
if self.current_value:
if len(self.current_value) == 1:
attrs["display_name"] = self.current_value[0].title
else:
attrs["display_name"] = (
f"Recently uploaded image(s): {[upload.title for upload in self.current_value]}"
)
attrs["widget_choices"] = {
choice.name: choice.value for choice in ImageWidgetChoices
}
return attrs
def compress(self, values):
if values:
non_empty_values = [
val for val in values if val and val not in self.empty_values
]
if len(non_empty_values) != 1:
raise ValidationError("Too many values returned.")
return non_empty_values[0]
DICOMUploadWidgetSuffixes = ["dicom-image-name", "dicom-user-uploads"]
class DICOMUploadWithName(NamedTuple):
name: str
user_uploads: list[
str
] # UserUpload pks, as expected by DICOMUserUploadMultipleWidget
class DICOMImageSetNameInput(TextInput):
template_name = "cases/dicom_image_set_name_input.html"
class DICOMUploadWidget(MultiWidget):
template_name = "cases/dicom_upload_widget.html"
def __init__(self, attrs=None):
widgets = {
DICOMUploadWidgetSuffixes[0]: DICOMImageSetNameInput(),
DICOMUploadWidgetSuffixes[1]: DICOMUserUploadMultipleWidget(),
}
super().__init__(widgets, attrs)
def decompress(self, value: DICOMUploadWithName):
if value:
return [
value.name,
value.user_uploads,
]
return ["", []]
class DICOMUploadField(MultiValueField):
widget = DICOMUploadWidget
def __init__(self, *args, user, initial=None, **kwargs):
upload_qs = filter_by_permission(
queryset=UserUpload.objects.all(),
user=user,
codename="change_userupload",
).filter(status=UserUpload.StatusChoices.COMPLETED)
fields = [
CharField(),
ModelMultipleChoiceField(queryset=upload_qs),
]
self.current_value = None
if initial:
# Initial data can only be an image CIV.
# We don't want to show the widgets in this case, and instead
# display the current image name, so pass the image as
# current_value to the widget template
if isinstance(initial, ComponentInterfaceValue):
if image := get_object_if_allowed(
model=Image,
pk=initial.image.pk,
user=user,
codename="view_image",
):
self.current_value = image
# turn initial to the internal data type that this widget expects
initial = self.compress(
values=[
image.name,
image.dicom_image_set.dicom_image_set_upload.user_uploads.all(),
]
)
else:
initial = None
else:
raise RuntimeError(
f"Unexpected initial value of type {type(initial)}"
)
super().__init__(
*args,
fields=fields,
initial=initial,
**kwargs,
)
def compress(self, values: list[str, QuerySet[UserUpload]]):
return DICOMUploadWithName(
name=values[0] if values else "",
user_uploads=[str(v.pk) for v in values[1]] if values else [],
)
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
attrs["current_value"] = self.current_value
return attrs