1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of SENAITE.CORE. |
4
|
|
|
# |
5
|
|
|
# SENAITE.CORE is free software: you can redistribute it and/or modify it under |
6
|
|
|
# the terms of the GNU General Public License as published by the Free Software |
7
|
|
|
# Foundation, version 2. |
8
|
|
|
# |
9
|
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT |
10
|
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
11
|
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
12
|
|
|
# details. |
13
|
|
|
# |
14
|
|
|
# You should have received a copy of the GNU General Public License along with |
15
|
|
|
# this program; if not, write to the Free Software Foundation, Inc., 51 |
16
|
|
|
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
17
|
|
|
# |
18
|
|
|
# Copyright 2018-2024 by it's authors. |
19
|
|
|
# Some rights reserved, see README and LICENSE. |
20
|
|
|
|
21
|
|
|
import re |
22
|
|
|
from string import Template |
23
|
|
|
|
24
|
|
|
from bika.lims import api |
25
|
|
|
from bika.lims import senaiteMessageFactory as _ |
26
|
|
|
from senaite.core.browser.form.adapters import EditFormAdapterBase |
27
|
|
|
from senaite.core.catalog import SETUP_CATALOG |
28
|
|
|
from senaite.core.i18n import translate |
29
|
|
|
|
30
|
|
|
pos_regex = re.compile(r"(\d+)\.widgets\.pos$") |
31
|
|
|
type_regex = re.compile(r"(\d+)\.widgets\.type$") |
32
|
|
|
dup_proxy_regex = re.compile(r"(\d+)\.widgets\.dup_proxy$") |
33
|
|
|
ref_proxy_regex = re.compile(r"(\d+)\.widgets\.reference_proxy$") |
34
|
|
|
|
35
|
|
|
POS_PARENT_SELECTOR = "td:has(>[name='{}'])" |
36
|
|
|
NUM_POS_SELECTOR = "#formfield-form-widgets-num_of_positions > .input-group" |
37
|
|
|
POS_DIV_BLOCK = "<div style='width: 85%; text-align: center;'>{}</div>" |
38
|
|
|
FIELD_NUM_POSITIONS = "form.widgets.num_of_positions" |
39
|
|
|
FIELD_LAYOUT = "form.widgets.template_layout" |
40
|
|
|
FIELD_POS = "form.widgets.template_layout.{}.widgets.pos" |
41
|
|
|
FIELD_TYPE = "form.widgets.template_layout.{}.widgets.type:list" |
42
|
|
|
FIELD_BLANK = "form.widgets.template_layout.{}.widgets.blank_ref" |
43
|
|
|
FIELD_CONTROL = "form.widgets.template_layout.{}.widgets.control_ref" |
44
|
|
|
FIELD_DUP = "form.widgets.template_layout.{}.widgets.dup" |
45
|
|
|
FIELD_DUP_PROXY = "form.widgets.template_layout.{}.widgets.dup_proxy:list" |
46
|
|
|
FIELD_REF_PROXY = "form.widgets.template_layout.{}.widgets.reference_proxy:list" |
47
|
|
|
|
48
|
|
|
NUM_POS_HTML = Template(""" |
49
|
|
|
<input |
50
|
|
|
type="submit" |
51
|
|
|
class="button-num-positions btn-primary" |
52
|
|
|
formaction="$absolute_url/@@update_num_positions" |
53
|
|
|
value="$title" |
54
|
|
|
/> |
55
|
|
|
""") |
56
|
|
|
|
57
|
|
|
|
58
|
|
|
class EditForm(EditFormAdapterBase): |
59
|
|
|
"""Edit form adapter for Worksheet Template |
60
|
|
|
""" |
61
|
|
|
|
62
|
|
|
def initialized(self, data): |
63
|
|
|
self.add_change_num_positions(data) |
64
|
|
|
self.init_toggle_fields(data) |
65
|
|
|
return self.data |
66
|
|
|
|
67
|
|
|
def init_toggle_fields(self, data): |
68
|
|
|
form = data.get("form") |
69
|
|
|
if int(form.get(FIELD_NUM_POSITIONS, "0")) == 0: |
70
|
|
|
self.toggle_field(FIELD_LAYOUT, False) |
71
|
|
|
else: |
72
|
|
|
self.modify_positions(data) |
73
|
|
|
count_rows = self.get_count_rows(data) |
74
|
|
|
for index in range(count_rows): |
75
|
|
|
field = FIELD_TYPE.format(index) |
76
|
|
|
analysis_type = form.get(field, "a") |
77
|
|
|
self.toggle_fields(data, analysis_type, index) |
78
|
|
|
|
79
|
|
|
def added(self, data): |
80
|
|
|
return self.data |
81
|
|
|
|
82
|
|
|
def callback(self, data): |
83
|
|
|
name = data.get("name") |
84
|
|
|
if not name: |
85
|
|
|
return |
86
|
|
|
method = getattr(self, name, None) |
87
|
|
|
if not callable(method): |
88
|
|
|
return |
89
|
|
|
return method(data) |
90
|
|
|
|
91
|
|
|
def modified(self, data): |
92
|
|
|
name = data.get("name") |
93
|
|
|
value = data.get("value") |
94
|
|
|
|
95
|
|
|
type_match = type_regex.search(name) |
96
|
|
|
dup_match = dup_proxy_regex.search(name) |
97
|
|
|
ref_match = ref_proxy_regex.search(name) |
98
|
|
|
|
99
|
|
|
if name == "form.widgets.restrict_to_method" and value: |
100
|
|
|
method_uid = value[0] |
101
|
|
|
options = self.get_instruments_options(method_uid) |
102
|
|
|
self.add_update_field("form.widgets.instrument", { |
103
|
|
|
"options": options |
104
|
|
|
}) |
105
|
|
|
elif type_match: |
106
|
|
|
idx = type_match.group(1) |
107
|
|
|
val = value[0] |
108
|
|
|
self.toggle_fields(data, val, idx) |
109
|
|
|
elif dup_match: |
110
|
|
|
idx = dup_match.group(1) |
111
|
|
|
if value: |
112
|
|
|
val = value[0] |
113
|
|
|
self.add_update_field(FIELD_DUP.format(idx), val) |
114
|
|
|
else: |
115
|
|
|
msg = translate(_( |
116
|
|
|
u"duplicate_reference_not_found", |
117
|
|
|
default=u"Not found Analysis position for duplicate.")) |
118
|
|
|
self.add_error_field(FIELD_DUP_PROXY.format(idx), msg) |
119
|
|
|
elif ref_match: |
120
|
|
|
idx = ref_match.group(1) |
121
|
|
|
form = data.get("form") |
122
|
|
|
analysis_type = form.get(FIELD_TYPE.format(idx)) |
123
|
|
|
if analysis_type == "b": |
124
|
|
|
self.add_update_field(FIELD_BLANK.format(idx), value) |
125
|
|
|
self.add_update_field(FIELD_CONTROL.format(idx), "") |
126
|
|
|
elif analysis_type == "c": |
127
|
|
|
self.add_update_field(FIELD_BLANK.format(idx), "") |
128
|
|
|
self.add_update_field(FIELD_CONTROL.format(idx), value) |
129
|
|
|
|
130
|
|
|
return self.data |
131
|
|
|
|
132
|
|
|
def toggle_fields(self, data, field_type, index): |
133
|
|
|
self.add_error_field(FIELD_TYPE.format(index), "") |
134
|
|
|
if field_type == "a": |
135
|
|
|
self.add_update_field(FIELD_BLANK.format(index), "") |
136
|
|
|
self.add_update_field(FIELD_CONTROL.format(index), "") |
137
|
|
|
self.add_update_field(FIELD_REF_PROXY.format(index), "") |
138
|
|
|
self.add_update_field(FIELD_DUP_PROXY.format(index), "") |
139
|
|
|
self.add_update_field(FIELD_DUP.format(index), "") |
140
|
|
|
|
141
|
|
|
self.toggle_reference_field(data, index, field_type) |
142
|
|
|
self.toggle_duplicate_field(data, index, field_type) |
143
|
|
|
|
144
|
|
|
def toggle_duplicate_field(self, data, index, field_type): |
145
|
|
|
field = FIELD_DUP_PROXY.format(index) |
146
|
|
|
toggle = field_type == "d" |
147
|
|
|
if toggle and self.validate_duplicate(data, index): |
148
|
|
|
self.update_duplicate_items(data, index) |
149
|
|
|
self.toggle_field(field, toggle) |
150
|
|
|
|
151
|
|
|
def validate_duplicate(self, data, index): |
152
|
|
|
form = data.get("form") |
153
|
|
|
current_pos = int(form.get(FIELD_POS.format(index))) |
154
|
|
|
count_rows = self.get_count_rows(data) |
155
|
|
|
dup_pos = 0 |
156
|
|
|
for i in range(count_rows): |
157
|
|
|
dup_value = form.get(FIELD_DUP.format(i)) |
158
|
|
|
if not dup_value: |
159
|
|
|
continue |
160
|
|
|
if int(dup_value) == current_pos: |
161
|
|
|
dup_pos = int(form.get(FIELD_POS.format(i))) |
162
|
|
|
break |
163
|
|
|
|
164
|
|
|
if dup_pos: |
165
|
|
|
msg = translate(_( |
166
|
|
|
u"duplicate_reference_this_position", |
167
|
|
|
default=u"Duplicate in position ${dup} references this, " |
168
|
|
|
u"so it must be a routine analysis.", |
169
|
|
|
mapping={"dup": dup_pos}) |
170
|
|
|
) |
171
|
|
|
self.add_error_field(FIELD_TYPE.format(index), msg) |
172
|
|
|
return False |
173
|
|
|
return True |
174
|
|
|
|
175
|
|
|
def toggle_reference_field(self, data, index, field_type): |
176
|
|
|
field = FIELD_REF_PROXY.format(index) |
177
|
|
|
toggle = field_type in ["b", "c"] |
178
|
|
|
field_ref = FIELD_BLANK if field_type == "b" else FIELD_CONTROL |
179
|
|
|
field_ref = field_ref.format(index) |
180
|
|
|
self.toggle_field(field, toggle) |
181
|
|
|
if toggle and self.validate_duplicate(data, index): |
182
|
|
|
form = data.get("form") |
183
|
|
|
options = self.get_reference_definitions(field_type) |
184
|
|
|
selected = form.get(field_ref) |
185
|
|
|
if len(options) and not selected: |
186
|
|
|
selected = options[0].get("value") |
187
|
|
|
self.add_update_field(field_ref, selected) |
188
|
|
|
|
189
|
|
|
self.add_update_field(field, { |
190
|
|
|
"options": options, |
191
|
|
|
"selected": selected, |
192
|
|
|
}) |
193
|
|
|
|
194
|
|
|
def toggle_field(self, field, toggle=False): |
195
|
|
|
if toggle: |
196
|
|
|
self.add_show_field(field) |
197
|
|
|
else: |
198
|
|
|
self.add_hide_field(field) |
199
|
|
|
|
200
|
|
|
def update_duplicate_items(self, data, index): |
201
|
|
|
"""Updating list of allowed duplicate values |
202
|
|
|
""" |
203
|
|
|
form = data.get("form") |
204
|
|
|
includes = set() |
205
|
|
|
count_rows = self.get_count_rows(data) |
206
|
|
|
for i in range(count_rows): |
207
|
|
|
select_type = form.get(FIELD_TYPE.format(i)) |
208
|
|
|
if select_type == "a": |
209
|
|
|
includes.add(form.get(FIELD_POS.format(i))) |
210
|
|
|
|
211
|
|
|
field_dup = FIELD_DUP.format(index) |
212
|
|
|
options = [dict(value=pos, title=pos) for pos in includes] |
213
|
|
|
selected = form.get(field_dup) |
214
|
|
|
if len(includes) and not selected: |
215
|
|
|
selected = next(iter(includes)) |
216
|
|
|
self.add_update_field(field_dup, selected) |
217
|
|
|
|
218
|
|
|
self.add_update_field(FIELD_DUP_PROXY.format(index), { |
219
|
|
|
"options": options, |
220
|
|
|
"selected": selected, |
221
|
|
|
}) |
222
|
|
|
|
223
|
|
|
def get_instruments_options(self, method): |
224
|
|
|
"""Returns a list of dicts that represent instrument options suitable |
225
|
|
|
for a selection list, with an empty option as first item |
226
|
|
|
""" |
227
|
|
|
options = [{ |
228
|
|
|
"title": _(u"form_widget_instrument_title", |
229
|
|
|
default=u"No Instrument"), |
230
|
|
|
"value": [""] |
231
|
|
|
}] |
232
|
|
|
method = api.get_object(method, default=None) |
233
|
|
|
instruments = method.getInstruments() if method else [] |
234
|
|
|
for instrument in instruments: |
235
|
|
|
option = { |
236
|
|
|
"title": api.get_title(instrument), |
237
|
|
|
"value": api.get_uid(instrument) |
238
|
|
|
} |
239
|
|
|
options.append(option) |
240
|
|
|
|
241
|
|
|
return options |
242
|
|
|
|
243
|
|
|
def add_change_num_positions(self, data): |
244
|
|
|
url = self.context.absolute_url() |
245
|
|
|
title = translate(_(u"num_of_positions_button_title", default=u"Set")) |
246
|
|
|
html = NUM_POS_HTML.safe_substitute(absolute_url=url, title=title) |
247
|
|
|
self.add_inner_html(NUM_POS_SELECTOR, html, append=True) |
248
|
|
|
|
249
|
|
|
def modify_positions(self, data): |
250
|
|
|
"""Replacing input control to text for positions |
251
|
|
|
""" |
252
|
|
|
count_rows = self.get_count_rows(data) |
253
|
|
|
for i in range(count_rows): |
254
|
|
|
field = FIELD_POS.format(i) |
255
|
|
|
pos = str(i + 1) |
256
|
|
|
self.add_update_field(field, pos) |
257
|
|
|
self.add_hide_field(field) |
258
|
|
|
selector = POS_PARENT_SELECTOR.format(field) |
259
|
|
|
html = POS_DIV_BLOCK.format(pos) |
260
|
|
|
self.add_inner_html(selector, html, append=True) |
261
|
|
|
|
262
|
|
|
def get_count_rows(self, data): |
263
|
|
|
form = data.get("form") |
264
|
|
|
positions = [k for k in form.keys() if pos_regex.search(k)] |
265
|
|
|
return len(positions) |
266
|
|
|
|
267
|
|
|
def get_reference_definitions(self, reference_type): |
268
|
|
|
reference_query = { |
269
|
|
|
"portal_type": "ReferenceDefinition", |
270
|
|
|
"is_active": True, |
271
|
|
|
} |
272
|
|
|
brains = api.search(reference_query, SETUP_CATALOG) |
273
|
|
|
definitions = map(api.get_object, brains) |
274
|
|
|
if reference_type == "b": |
275
|
|
|
definitions = filter(lambda d: d.getBlank(), definitions) |
276
|
|
|
else: |
277
|
|
|
definitions = filter(lambda d: not d.getBlank(), definitions) |
278
|
|
|
return [dict(value=d.UID(), title=d.Title()) for d in definitions] |
279
|
|
|
|