Total Complexity | 59 |
Total Lines | 902 |
Duplicated Lines | 16.85 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like bika.lims.browser.instrument often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
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-2025 by it's authors. |
||
19 | # Some rights reserved, see README and LICENSE. |
||
20 | |||
21 | import json |
||
22 | |||
23 | import plone |
||
24 | from bika.lims import api |
||
25 | from bika.lims import bikaMessageFactory as _ |
||
26 | from bika.lims.browser import BrowserView |
||
27 | from bika.lims.browser.analyses import AnalysesView |
||
28 | from bika.lims.browser.bika_listing import BikaListingView |
||
29 | from bika.lims.browser.chart.analyses import EvolutionChart |
||
30 | from bika.lims.browser.resultsimport.autoimportlogs import AutoImportLogsView |
||
31 | from bika.lims.content.instrumentmaintenancetask import \ |
||
32 | InstrumentMaintenanceTaskStatuses as mstatus |
||
33 | from bika.lims.utils import get_image |
||
34 | from bika.lims.utils import get_link |
||
35 | from bika.lims.utils import get_link_for |
||
36 | from senaite.core.i18n import translate as t |
||
37 | from plone.app.layout.globals.interfaces import IViewView |
||
38 | from Products.CMFCore.utils import getToolByName |
||
39 | from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
||
40 | from senaite.core.catalog import ANALYSIS_CATALOG |
||
41 | from senaite.core.catalog import SETUP_CATALOG |
||
42 | from zExceptions import Forbidden |
||
43 | from ZODB.POSException import POSKeyError |
||
44 | from zope.interface import implements |
||
45 | |||
46 | |||
47 | class InstrumentMaintenanceView(BikaListingView): |
||
48 | """Listing view for instrument maintenance tasks |
||
49 | """ |
||
50 | |||
51 | View Code Duplication | def __init__(self, context, request): |
|
|
|||
52 | super(InstrumentMaintenanceView, self).__init__(context, request) |
||
53 | self.catalog = SETUP_CATALOG |
||
54 | self.contentFilter = { |
||
55 | "portal_type": "InstrumentMaintenanceTask", |
||
56 | "path": { |
||
57 | "query": api.get_path(context), |
||
58 | "depth": 1 # searching just inside the specified folder |
||
59 | }, |
||
60 | "sort_on": "created", |
||
61 | "sort_order": "descending", |
||
62 | } |
||
63 | |||
64 | self.form_id = "instrumentmaintenance" |
||
65 | self.title = self.context.translate(_("Instrument Maintenance")) |
||
66 | |||
67 | self.icon = "{}/{}".format( |
||
68 | self.portal_url, |
||
69 | "++resource++bika.lims.images/instrumentmaintenance_big.png" |
||
70 | ) |
||
71 | self.context_actions = { |
||
72 | _("Add"): { |
||
73 | "url": "createObject?type_name=InstrumentMaintenanceTask", |
||
74 | "icon": "++resource++bika.lims.images/add.png"} |
||
75 | } |
||
76 | |||
77 | self.allow_edit = False |
||
78 | self.show_select_column = False |
||
79 | self.show_workflow_action_buttons = True |
||
80 | self.pagesize = 30 |
||
81 | |||
82 | self.columns = { |
||
83 | 'getCurrentState': {'title': ''}, |
||
84 | 'Title': {'title': _('Task'), |
||
85 | 'index': 'sortable_title'}, |
||
86 | 'getType': {'title': _('Task type', 'Type'), 'sortable': True}, |
||
87 | 'getDownFrom': {'title': _('Down from'), 'sortable': True}, |
||
88 | 'getDownTo': {'title': _('Down to'), 'sortable': True}, |
||
89 | 'getMaintainer': {'title': _('Maintainer'), 'sortable': True}, |
||
90 | } |
||
91 | |||
92 | self.review_states = [ |
||
93 | { |
||
94 | "id": "default", |
||
95 | "title": _("Open"), |
||
96 | "contentFilter": {"is_active": True}, |
||
97 | "columns": [ |
||
98 | "getCurrentState", |
||
99 | "Title", |
||
100 | "getType", |
||
101 | "getDownFrom", |
||
102 | "getDownTo", |
||
103 | "getMaintainer", |
||
104 | ] |
||
105 | }, { |
||
106 | "id": "cancelled", |
||
107 | "title": _("Cancelled"), |
||
108 | "contentFilter": {"is_active": False}, |
||
109 | "columns": [ |
||
110 | "getCurrentState", |
||
111 | "Title", |
||
112 | "getType", |
||
113 | "getDownFrom", |
||
114 | "getDownTo", |
||
115 | "getMaintainer", |
||
116 | ] |
||
117 | }, { |
||
118 | "id": "all", |
||
119 | "title": _("All"), |
||
120 | "contentFilter": {}, |
||
121 | "columns": [ |
||
122 | "getCurrentState", |
||
123 | "Title", |
||
124 | "getType", |
||
125 | "getDownFrom", |
||
126 | "getDownTo", |
||
127 | "getMaintainer" |
||
128 | ] |
||
129 | } |
||
130 | ] |
||
131 | |||
132 | def localize_date(self, date): |
||
133 | """Return the localized date |
||
134 | """ |
||
135 | return self.ulocalized_time(date, long_format=1) |
||
136 | |||
137 | def folderitem(self, obj, item, index): |
||
138 | """Augment folder listing item |
||
139 | """ |
||
140 | obj = api.get_object(obj) |
||
141 | url = item.get("url") |
||
142 | title = item.get("Title") |
||
143 | |||
144 | item["replace"]["Title"] = get_link(url, value=title) |
||
145 | item["getType"] = _(obj.getType()[0]) |
||
146 | item["getDownFrom"] = self.localize_date(obj.getDownFrom()) |
||
147 | item["getDownTo"] = self.localize_date(obj.getDownTo()) |
||
148 | item["getMaintainer"] = obj.getMaintainer() |
||
149 | |||
150 | status = obj.getCurrentState() |
||
151 | statustext = obj.getCurrentStateI18n() |
||
152 | statusimg = "" |
||
153 | |||
154 | if status == mstatus.CLOSED: |
||
155 | statusimg = "instrumentmaintenance_closed.png" |
||
156 | item["state_class"] = "state-inactive" |
||
157 | elif status == mstatus.CANCELLED: |
||
158 | statusimg = "instrumentmaintenance_cancelled.png" |
||
159 | item["state_class"] = "state-cancelled" |
||
160 | elif status == mstatus.INQUEUE: |
||
161 | statusimg = "instrumentmaintenance_inqueue.png" |
||
162 | item["state_class"] = "state-open" |
||
163 | elif status == mstatus.OVERDUE: |
||
164 | statusimg = "instrumentmaintenance_overdue.png" |
||
165 | item["state_class"] = "state-open" |
||
166 | elif status == mstatus.PENDING: |
||
167 | statusimg = "instrumentmaintenance_pending.png" |
||
168 | item["state_class"] = "state-pending" |
||
169 | |||
170 | item["replace"]["getCurrentState"] = get_image( |
||
171 | statusimg, title=statustext) |
||
172 | return item |
||
173 | |||
174 | |||
175 | class InstrumentCalibrationsView(BikaListingView): |
||
176 | """Listing view for instrument calibrations |
||
177 | """ |
||
178 | |||
179 | def __init__(self, context, request): |
||
180 | super(InstrumentCalibrationsView, self).__init__(context, request) |
||
181 | self.catalog = SETUP_CATALOG |
||
182 | self.contentFilter = { |
||
183 | "portal_type": "InstrumentCalibration", |
||
184 | "path": { |
||
185 | "query": api.get_path(context), |
||
186 | "depth": 1 # searching just inside the specified folder |
||
187 | }, |
||
188 | "sort_on": "created", |
||
189 | "sort_order": "descending", |
||
190 | } |
||
191 | |||
192 | self.form_id = "instrumentcalibrations" |
||
193 | self.title = self.context.translate(_("Instrument Calibrations")) |
||
194 | self.icon = "{}/{}".format( |
||
195 | self.portal_url, |
||
196 | "++resource++bika.lims.images/instrumentcalibration_big.png" |
||
197 | ) |
||
198 | self.context_actions = { |
||
199 | _("Add"): { |
||
200 | "url": "createObject?type_name=InstrumentCalibration", |
||
201 | "icon": "++resource++bika.lims.images/add.png"} |
||
202 | } |
||
203 | |||
204 | self.allow_edit = False |
||
205 | self.show_select_column = False |
||
206 | self.show_workflow_action_buttons = True |
||
207 | self.pagesize = 30 |
||
208 | |||
209 | # instrument calibrations |
||
210 | calibrations = self.context.getCalibrations() |
||
211 | # current running calibrations |
||
212 | self.active_calibrations = filter( |
||
213 | lambda c: c.isCalibrationInProgress(), calibrations) |
||
214 | self.latest_calibration = self.context.getLatestValidCalibration() |
||
215 | |||
216 | self.columns = { |
||
217 | "Title": {"title": _("Task"), |
||
218 | "index": "sortable_title"}, |
||
219 | "getDownFrom": {"title": _("Down from")}, |
||
220 | "getDownTo": {"title": _("Down to")}, |
||
221 | "getCalibrator": {"title": _("Calibrator")}, |
||
222 | } |
||
223 | self.review_states = [ |
||
224 | { |
||
225 | "id": "default", |
||
226 | "title": _("All"), |
||
227 | "contentFilter": {}, |
||
228 | "columns": [ |
||
229 | "Title", |
||
230 | "getDownFrom", |
||
231 | "getDownTo", |
||
232 | "getCalibrator", |
||
233 | ] |
||
234 | } |
||
235 | ] |
||
236 | |||
237 | def localize_date(self, date): |
||
238 | """Return the localized date |
||
239 | """ |
||
240 | return self.ulocalized_time(date, long_format=1) |
||
241 | |||
242 | def folderitem(self, obj, item, index): |
||
243 | """Augment folder listing item |
||
244 | """ |
||
245 | obj = api.get_object(obj) |
||
246 | url = item.get("url") |
||
247 | title = item.get("Title") |
||
248 | calibrator = obj.getCalibrator() |
||
249 | |||
250 | item["getDownFrom"] = self.localize_date(obj.getDownFrom()) |
||
251 | item["getDownTo"] = self.localize_date(obj.getDownTo()) |
||
252 | item["getCalibrator"] = "" |
||
253 | if calibrator: |
||
254 | props = api.get_user_properties(calibrator) |
||
255 | name = props.get("fullname", calibrator) |
||
256 | item["getCalibrator"] = name |
||
257 | item["replace"]["Title"] = get_link(url, value=title) |
||
258 | |||
259 | # calibration with the most remaining days |
||
260 | if obj == self.latest_calibration: |
||
261 | item["state_class"] = "state-published" |
||
262 | # running calibrations |
||
263 | elif obj in self.active_calibrations: |
||
264 | item["state_class"] = "state-active" |
||
265 | # inactive calibrations |
||
266 | else: |
||
267 | item["state_class"] = "state-inactive" |
||
268 | |||
269 | return item |
||
270 | |||
271 | |||
272 | class InstrumentValidationsView(BikaListingView): |
||
273 | """Listing view for instrument validations |
||
274 | """ |
||
275 | |||
276 | def __init__(self, context, request): |
||
277 | super(InstrumentValidationsView, self).__init__(context, request) |
||
278 | self.catalog = SETUP_CATALOG |
||
279 | self.contentFilter = { |
||
280 | "portal_type": "InstrumentValidation", |
||
281 | "path": { |
||
282 | "query": api.get_path(context), |
||
283 | "depth": 1 # searching just inside the specified folder |
||
284 | }, |
||
285 | "sort_on": "created", |
||
286 | "sort_order": "descending", |
||
287 | } |
||
288 | |||
289 | self.form_id = "instrumentvalidations" |
||
290 | self.title = self.context.translate(_("Instrument Validations")) |
||
291 | self.icon = "{}/{}".format( |
||
292 | self.portal_url, |
||
293 | "++resource++bika.lims.images/instrumentvalidation_big.png" |
||
294 | ) |
||
295 | self.context_actions = { |
||
296 | _("Add"): { |
||
297 | "url": "createObject?type_name=InstrumentValidation", |
||
298 | "icon": "++resource++bika.lims.images/add.png"} |
||
299 | } |
||
300 | |||
301 | self.allow_edit = False |
||
302 | self.show_select_column = False |
||
303 | self.show_workflow_action_buttons = True |
||
304 | self.pagesize = 30 |
||
305 | |||
306 | # instrument validations |
||
307 | validations = self.context.getValidations() |
||
308 | # current running validations |
||
309 | self.active_validations = filter( |
||
310 | lambda v: v.isValidationInProgress(), validations) |
||
311 | self.latest_validation = self.context.getLatestValidValidation() |
||
312 | |||
313 | self.columns = { |
||
314 | "Title": {"title": _("Task"), |
||
315 | "index": "sortable_title"}, |
||
316 | "getDownFrom": {"title": _("Down from")}, |
||
317 | "getDownTo": {"title": _("Down to")}, |
||
318 | "getValidator": {"title": _("Validator")}, |
||
319 | } |
||
320 | self.review_states = [ |
||
321 | { |
||
322 | "id": "default", |
||
323 | "title": _("All"), |
||
324 | "contentFilter": {}, |
||
325 | "columns": [ |
||
326 | "Title", |
||
327 | "getDownFrom", |
||
328 | "getDownTo", |
||
329 | "getValidator", |
||
330 | ] |
||
331 | } |
||
332 | ] |
||
333 | |||
334 | def localize_date(self, date): |
||
335 | """Return the localized date |
||
336 | """ |
||
337 | return self.ulocalized_time(date, long_format=1) |
||
338 | |||
339 | def folderitem(self, obj, item, index): |
||
340 | """Augment folder listing item |
||
341 | """ |
||
342 | obj = api.get_object(obj) |
||
343 | url = item.get("url") |
||
344 | title = item.get("Title") |
||
345 | |||
346 | item["getDownFrom"] = self.localize_date(obj.getDownFrom()) |
||
347 | item["getDownTo"] = self.localize_date(obj.getDownTo()) |
||
348 | item["getValidator"] = obj.getValidator() |
||
349 | item["replace"]["Title"] = get_link(url, value=title) |
||
350 | |||
351 | # validation with the most remaining days |
||
352 | if obj == self.latest_validation: |
||
353 | item["state_class"] = "state-published" |
||
354 | # running validations |
||
355 | elif obj in self.active_validations: |
||
356 | item["state_class"] = "state-active" |
||
357 | # inactive validations |
||
358 | else: |
||
359 | item["state_class"] = "state-inactive" |
||
360 | |||
361 | return item |
||
362 | |||
363 | |||
364 | class InstrumentScheduleView(BikaListingView): |
||
365 | """Listing view for instrument scheduled tasks |
||
366 | """ |
||
367 | |||
368 | View Code Duplication | def __init__(self, context, request): |
|
369 | super(InstrumentScheduleView, self).__init__(context, request) |
||
370 | self.catalog = SETUP_CATALOG |
||
371 | self.contentFilter = { |
||
372 | "portal_type": "InstrumentScheduledTask", |
||
373 | "path": { |
||
374 | "query": api.get_path(context), |
||
375 | "depth": 1 # searching just inside the specified folder |
||
376 | }, |
||
377 | "sort_on": "created", |
||
378 | "sort_order": "descending", |
||
379 | } |
||
380 | |||
381 | self.form_id = "instrumentschedule" |
||
382 | self.title = self.context.translate(_("Instrument Scheduled Tasks")) |
||
383 | |||
384 | self.icon = "{}/{}".format( |
||
385 | self.portal_url, |
||
386 | "++resource++bika.lims.images/instrumentschedule_big.png" |
||
387 | ) |
||
388 | self.context_actions = { |
||
389 | _("Add"): { |
||
390 | "url": "createObject?type_name=InstrumentScheduledTask", |
||
391 | "icon": "++resource++bika.lims.images/add.png"} |
||
392 | } |
||
393 | |||
394 | self.allow_edit = False |
||
395 | self.show_select_column = False |
||
396 | self.show_workflow_action_buttons = True |
||
397 | self.pagesize = 30 |
||
398 | |||
399 | self.columns = { |
||
400 | "Title": {"title": _("Scheduled task"), |
||
401 | "index": "sortable_title"}, |
||
402 | "getType": {"title": _("Task type", "Type")}, |
||
403 | "getCriteria": {"title": _("Criteria")}, |
||
404 | "creator": {"title": _("Created by")}, |
||
405 | "created": {"title": _("Created")}, |
||
406 | } |
||
407 | |||
408 | self.review_states = [ |
||
409 | { |
||
410 | "id": "default", |
||
411 | "title": _("Active"), |
||
412 | "contentFilter": {"is_active": True}, |
||
413 | "transitions": [{"id": "deactivate"}, ], |
||
414 | "columns": [ |
||
415 | "Title", |
||
416 | "getType", |
||
417 | "getCriteria", |
||
418 | "creator", |
||
419 | "created", |
||
420 | ] |
||
421 | }, { |
||
422 | "id": "inactive", |
||
423 | "title": _("Inactive"), |
||
424 | "contentFilter": {'is_active': False}, |
||
425 | "transitions": [{"id": "activate"}, ], |
||
426 | "columns": [ |
||
427 | "Title", |
||
428 | "getType", |
||
429 | "getCriteria", |
||
430 | "creator", |
||
431 | "created" |
||
432 | ] |
||
433 | }, { |
||
434 | "id": "all", |
||
435 | "title": _("All"), |
||
436 | "contentFilter": {}, |
||
437 | "columns": [ |
||
438 | "Title", |
||
439 | "getType", |
||
440 | "getCriteria", |
||
441 | "creator", |
||
442 | "created", |
||
443 | ] |
||
444 | } |
||
445 | ] |
||
446 | |||
447 | def localize_date(self, date): |
||
448 | """Return the localized date |
||
449 | """ |
||
450 | return self.ulocalized_time(date, long_format=1) |
||
451 | |||
452 | def folderitem(self, obj, item, index): |
||
453 | """Augment folder listing item |
||
454 | """ |
||
455 | obj = api.get_object(obj) |
||
456 | url = item.get("url") |
||
457 | title = item.get("Title") |
||
458 | creator = obj.Creator() |
||
459 | |||
460 | item["replace"]["Title"] = get_link(url, value=title) |
||
461 | item["created"] = self.localize_date(obj.created()) |
||
462 | item["getType"] = _(obj.getType()[0]) |
||
463 | item["creator"] = "" |
||
464 | if creator: |
||
465 | props = api.get_user_properties(creator) |
||
466 | name = props.get("fullname", creator) |
||
467 | item["creator"] = name |
||
468 | |||
469 | return item |
||
470 | |||
471 | |||
472 | class InstrumentReferenceAnalysesViewView(BrowserView): |
||
473 | """View of Reference Analyses linked to the Instrument. |
||
474 | |||
475 | Only shows the Reference Analyses (Control and Blanks), the rest of regular |
||
476 | and duplicate analyses linked to this instrument are not displayed. |
||
477 | |||
478 | The Reference Analyses from an Instrument can be from Worksheets (QC |
||
479 | analysis performed regularly for any Analysis Request) or attached directly |
||
480 | to the instrument, without being linked to any Worksheet). |
||
481 | |||
482 | In this case, the Reference Analyses are created automatically by the |
||
483 | instrument import tool. |
||
484 | """ |
||
485 | |||
486 | implements(IViewView) |
||
487 | template = ViewPageTemplateFile( |
||
488 | "templates/instrument_referenceanalyses.pt") |
||
489 | |||
490 | def __init__(self, context, request): |
||
491 | super(InstrumentReferenceAnalysesViewView, self).__init__( |
||
492 | context, request) |
||
493 | |||
494 | self.title = self.context.translate(_("Internal Calibration Tests")) |
||
495 | self.icon = "{}/{}".format( |
||
496 | self.portal_url, |
||
497 | "++resource++bika.lims.images/referencesample_big.png" |
||
498 | ) |
||
499 | self._analysesview = None |
||
500 | |||
501 | def __call__(self): |
||
502 | return self.template() |
||
503 | |||
504 | def get_analyses_table_view(self): |
||
505 | view_name = "table_instrument_referenceanalyses" |
||
506 | view = api.get_view( |
||
507 | view_name, context=self.context, request=self.request) |
||
508 | # Call listing hooks |
||
509 | view.update() |
||
510 | view.before_render() |
||
511 | |||
512 | # TODO Refactor QC Charts as React Components |
||
513 | # The current QC Chart is rendered by looking at the value from a hidden |
||
514 | # input with id "graphdata", that is rendered below the contents listing |
||
515 | # (see instrument_referenceanalyses.pt). |
||
516 | # The value is a json, is built by folderitem function and is returned |
||
517 | # by self.chart.get_json(). This function is called directly by the |
||
518 | # template on render, but the template itself does not directly render |
||
519 | # the contents listing, but is done asyncronously. |
||
520 | # Hence the function at this point returns an empty dictionary because |
||
521 | # folderitems hasn't been called yet. As a result, the chart appears |
||
522 | # empty. Here, we force folderitems function to be called in order to |
||
523 | # ensure the graphdata is filled before the template is rendered. |
||
524 | view.get_folderitems() |
||
525 | return view |
||
526 | |||
527 | |||
528 | class InstrumentReferenceAnalysesView(AnalysesView): |
||
529 | """View for the table of Reference Analyses linked to the Instrument. |
||
530 | |||
531 | Only shows the Reference Analyses (Control and Blanks), the rest of regular |
||
532 | and duplicate analyses linked to this instrument are not displayed. |
||
533 | """ |
||
534 | |||
535 | def __init__(self, context, request, **kwargs): |
||
536 | AnalysesView.__init__(self, context, request, **kwargs) |
||
537 | |||
538 | self.form_id = "{}_qcanalyses".format(api.get_uid(context)) |
||
539 | self.allow_edit = True |
||
540 | self.show_select_column = True |
||
541 | self.show_search = False |
||
542 | |||
543 | self.catalog = ANALYSIS_CATALOG |
||
544 | |||
545 | self.contentFilter = { |
||
546 | "portal_type": "ReferenceAnalysis", |
||
547 | "getInstrumentUID": api.get_uid(self.context), |
||
548 | "sort_on": "getResultCaptureDate", |
||
549 | "sort_order": "reverse" |
||
550 | } |
||
551 | |||
552 | # insert the QC-specific columns |
||
553 | self.add_column("getReferenceAnalysesGroupID", |
||
554 | title=_("QC Sample ID"), |
||
555 | after="Service") |
||
556 | |||
557 | self.add_column("Partition", |
||
558 | title=_("Reference Sample"), |
||
559 | after="getReferenceAnalysesGroupID") |
||
560 | |||
561 | self.add_column("Retractions", title=_("Retractions")) |
||
562 | |||
563 | self.chart = EvolutionChart() |
||
564 | |||
565 | def add_column(self, id, **kwargs): |
||
566 | """Add the given column to all review states |
||
567 | """ |
||
568 | after = kwargs.pop("after", "") |
||
569 | self.columns[id] = kwargs |
||
570 | for rv in self.review_states: |
||
571 | cols = rv["columns"] |
||
572 | index = len(cols) |
||
573 | if after and after in cols: |
||
574 | index = cols.index(after) + 1 |
||
575 | cols.insert(index, id) |
||
576 | |||
577 | def isItemAllowed(self, obj): |
||
578 | allowed = super(InstrumentReferenceAnalysesView, |
||
579 | self).isItemAllowed(obj) |
||
580 | return allowed or obj.getResult != "" |
||
581 | |||
582 | def folderitem(self, obj, item, index): |
||
583 | item = super(InstrumentReferenceAnalysesView, |
||
584 | self).folderitem(obj, item, index) |
||
585 | |||
586 | # get the full object |
||
587 | analysis = self.get_object(obj) |
||
588 | |||
589 | # Partition is used to group/toggle QC Analyses |
||
590 | sample = analysis.getSample() |
||
591 | item["replace"]["Partition"] = get_link(api.get_url(sample), |
||
592 | api.get_id(sample)) |
||
593 | |||
594 | # Get retractions field |
||
595 | item["Retractions"] = "" |
||
596 | report = analysis.getRetractedAnalysesPdfReport() |
||
597 | if report: |
||
598 | url = api.get_url(analysis) |
||
599 | href = "{}/at_download/RetractedAnalysesPdfReport".format(url) |
||
600 | attrs = {"class": "pdf", "target": "_blank"} |
||
601 | title = _("Retractions") |
||
602 | link = get_link(href, title, **attrs) |
||
603 | item["Retractions"] = title |
||
604 | item["replace"]["Retractions"] = link |
||
605 | |||
606 | # Add the analysis to the QC Chart |
||
607 | self.chart.add_analysis(analysis) |
||
608 | |||
609 | return item |
||
610 | |||
611 | def _folder_item_instrument(self, analysis_brain, item): |
||
612 | """Always display the current instrument in read-only mode |
||
613 | """ |
||
614 | instrument = self.get_instrument(analysis_brain) |
||
615 | item["Instrument"] = api.get_uid(instrument) |
||
616 | item["replace"]["Instrument"] = get_link_for(instrument, tabindex="-1") |
||
617 | |||
618 | |||
619 | class InstrumentCertificationsView(BikaListingView): |
||
620 | """Listing view for instrument certifications |
||
621 | """ |
||
622 | |||
623 | def __init__(self, context, request, **kwargs): |
||
624 | BikaListingView.__init__(self, context, request, **kwargs) |
||
625 | self.catalog = SETUP_CATALOG |
||
626 | self.contentFilter = { |
||
627 | "portal_type": "InstrumentCertification", |
||
628 | "path": { |
||
629 | "query": api.get_path(context), |
||
630 | "depth": 1 # searching just inside the specified folder |
||
631 | }, |
||
632 | "sort_on": "created", |
||
633 | "sort_order": "descending", |
||
634 | } |
||
635 | |||
636 | self.form_id = "instrumentcertifications" |
||
637 | self.title = self.context.translate(_("Calibration Certificates")) |
||
638 | self.icon = "{}/{}".format( |
||
639 | self.portal_url, |
||
640 | "++resource++bika.lims.images/instrumentcertification_big.png" |
||
641 | ) |
||
642 | self.context_actions = { |
||
643 | _("Add"): { |
||
644 | "url": "createObject?type_name=InstrumentCertification", |
||
645 | "icon": "++resource++bika.lims.images/add.png" |
||
646 | } |
||
647 | } |
||
648 | |||
649 | self.allow_edit = False |
||
650 | self.show_select_column = False |
||
651 | self.show_workflow_action_buttons = True |
||
652 | self.pagesize = 30 |
||
653 | |||
654 | # latest valid certificate UIDs |
||
655 | self.valid_certificates = self.context.getValidCertifications() |
||
656 | self.latest_certificate = self.context.getLatestValidCertification() |
||
657 | |||
658 | self.columns = { |
||
659 | "Title": {"title": _("Cert. Num"), "index": "sortable_title"}, |
||
660 | "getAgency": {"title": _("Agency"), "sortable": False}, |
||
661 | "getDate": {"title": _("Date"), "sortable": False}, |
||
662 | "getValidFrom": {"title": _("Valid from"), "sortable": False}, |
||
663 | "getValidTo": {"title": _("Valid to"), "sortable": False}, |
||
664 | "getDocument": {"title": _("Document"), "sortable": False}, |
||
665 | } |
||
666 | |||
667 | self.review_states = [ |
||
668 | { |
||
669 | "id": "default", |
||
670 | "title": _("All"), |
||
671 | "contentFilter": {}, |
||
672 | "columns": [ |
||
673 | "Title", |
||
674 | "getAgency", |
||
675 | "getDate", |
||
676 | "getValidFrom", |
||
677 | "getValidTo", |
||
678 | "getDocument", |
||
679 | ], |
||
680 | "transitions": [] |
||
681 | } |
||
682 | ] |
||
683 | |||
684 | def get_document(self, certificate): |
||
685 | """Return the document of the given document |
||
686 | """ |
||
687 | try: |
||
688 | return certificate.getDocument() |
||
689 | except POSKeyError: # POSKeyError: "No blob file" |
||
690 | # XXX When does this happen? |
||
691 | return None |
||
692 | |||
693 | def localize_date(self, date): |
||
694 | """Return the localized date |
||
695 | """ |
||
696 | return self.ulocalized_time(date, long_format=0) |
||
697 | |||
698 | def folderitem(self, obj, item, index): |
||
699 | """Augment folder listing item with additional data |
||
700 | """ |
||
701 | obj = api.get_object(obj) |
||
702 | url = item.get("url") |
||
703 | title = item.get("Title") |
||
704 | |||
705 | item["replace"]["Title"] = get_link(url, value=title) |
||
706 | item["getDate"] = self.localize_date(obj.getDate()) |
||
707 | item["getValidFrom"] = self.localize_date(obj.getValidFrom()) |
||
708 | item["getValidTo"] = self.localize_date(obj.getValidTo()) |
||
709 | |||
710 | if obj.getInternal() is True: |
||
711 | item["replace"]["getAgency"] = "" |
||
712 | item["state_class"] = "%s %s" % \ |
||
713 | (item["state_class"], "internalcertificate") |
||
714 | |||
715 | item["getDocument"] = "" |
||
716 | item["replace"]["getDocument"] = "" |
||
717 | doc = self.get_document(obj) |
||
718 | if doc and doc.get_size() > 0: |
||
719 | filename = doc.filename |
||
720 | download_url = "{}/at_download/Document".format(url) |
||
721 | anchor = get_link(download_url, filename) |
||
722 | item["getDocument"] = filename |
||
723 | item["replace"]["getDocument"] = anchor |
||
724 | |||
725 | # Latest valid certificate |
||
726 | if obj == self.latest_certificate: |
||
727 | item["state_class"] = "state-published" |
||
728 | # Valid certificate |
||
729 | elif obj in self.valid_certificates: |
||
730 | item["state_class"] = "state-valid state-published" |
||
731 | # Invalid certificates |
||
732 | else: |
||
733 | img = get_image("exclamation.png", title=t(_("Out of date"))) |
||
734 | item["replace"]["getValidTo"] = "%s %s" % (item["getValidTo"], img) |
||
735 | item["state_class"] = "state-invalid" |
||
736 | |||
737 | return item |
||
738 | |||
739 | |||
740 | class InstrumentAutoImportLogsView(AutoImportLogsView): |
||
741 | """Logs of Auto-Imports of this instrument. |
||
742 | """ |
||
743 | |||
744 | def __init__(self, context, request, **kwargs): |
||
745 | AutoImportLogsView.__init__(self, context, request, **kwargs) |
||
746 | del self.columns["Instrument"] |
||
747 | self.review_states[0]["columns"].remove("Instrument") |
||
748 | self.contentFilter = { |
||
749 | "portal_type": "AutoImportLog", |
||
750 | "path": { |
||
751 | "query": api.get_path(context), |
||
752 | }, |
||
753 | "sort_on": "created", |
||
754 | "sort_order": "descending", |
||
755 | } |
||
756 | |||
757 | instrument = self.context.Title() |
||
758 | self.title = self.context.translate( |
||
759 | _(u"Auto Import Logs of %s" % api.safe_unicode(instrument))) |
||
760 | self.icon = "{}/{}".format( |
||
761 | self.portal_url, |
||
762 | "++resource++bika.lims.images/instrumentcertification_big.png" |
||
763 | ) |
||
764 | self.context_actions = {} |
||
765 | |||
766 | self.allow_edit = False |
||
767 | self.show_select_column = False |
||
768 | self.show_workflow_action_buttons = True |
||
769 | self.pagesize = 30 |
||
770 | |||
771 | |||
772 | class InstrumentMultifileView(BikaListingView): |
||
773 | """Listing view for instrument multi files |
||
774 | """ |
||
775 | |||
776 | def __init__(self, context, request): |
||
777 | super(InstrumentMultifileView, self).__init__(context, request) |
||
778 | |||
779 | self.catalog = "senaite_catalog_setup" |
||
780 | self.contentFilter = { |
||
781 | "portal_type": "Multifile", |
||
782 | "path": { |
||
783 | "query": api.get_path(context), |
||
784 | "depth": 1 # searching just inside the specified folder |
||
785 | }, |
||
786 | "sort_on": "created", |
||
787 | "sort_order": "descending", |
||
788 | } |
||
789 | |||
790 | self.form_id = "instrumentfiles" |
||
791 | self.title = self.context.translate(_("Instrument Files")) |
||
792 | self.icon = "{}/{}".format( |
||
793 | self.portal_url, |
||
794 | "++resource++bika.lims.images/instrumentcertification_big.png" |
||
795 | ) |
||
796 | self.context_actions = { |
||
797 | _("Add"): { |
||
798 | "url": "createObject?type_name=Multifile", |
||
799 | "icon": "++resource++bika.lims.images/add.png" |
||
800 | } |
||
801 | } |
||
802 | |||
803 | self.allow_edit = False |
||
804 | self.show_select_column = False |
||
805 | self.show_workflow_action_buttons = True |
||
806 | self.pagesize = 30 |
||
807 | |||
808 | self.columns = { |
||
809 | "DocumentID": {"title": _("Document ID"), |
||
810 | "index": "sortable_title"}, |
||
811 | "DocumentVersion": {"title": _("Document Version"), |
||
812 | "index": "sortable_title"}, |
||
813 | "DocumentLocation": {"title": _("Document Location"), |
||
814 | "index": "sortable_title"}, |
||
815 | "DocumentType": {"title": _("Document Type"), |
||
816 | "index": "sortable_title"}, |
||
817 | "FileDownload": {"title": _("File")} |
||
818 | } |
||
819 | |||
820 | self.review_states = [ |
||
821 | { |
||
822 | "id": "default", |
||
823 | "title": _("All"), |
||
824 | "contentFilter": {}, |
||
825 | "columns": [ |
||
826 | "DocumentID", |
||
827 | "DocumentVersion", |
||
828 | "DocumentLocation", |
||
829 | "DocumentType", |
||
830 | "FileDownload" |
||
831 | ] |
||
832 | }, |
||
833 | ] |
||
834 | |||
835 | def get_file(self, obj): |
||
836 | """Return the file of the given object |
||
837 | """ |
||
838 | try: |
||
839 | return obj.getFile() |
||
840 | except POSKeyError: # POSKeyError: "No blob file" |
||
841 | # XXX When does this happen? |
||
842 | return None |
||
843 | |||
844 | def folderitem(self, obj, item, index): |
||
845 | """Augment folder listing item with additional data |
||
846 | """ |
||
847 | obj = api.get_object(obj) |
||
848 | url = item.get("url") |
||
849 | title = obj.getDocumentID() |
||
850 | |||
851 | item["replace"]["DocumentID"] = get_link(url, title) |
||
852 | |||
853 | item["FileDownload"] = "" |
||
854 | item["replace"]["FileDownload"] = "" |
||
855 | file = self.get_file(obj) |
||
856 | if file and file.get_size() > 0: |
||
857 | filename = file.filename |
||
858 | download_url = "{}/at_download/File".format(url) |
||
859 | anchor = get_link(download_url, filename) |
||
860 | item["FileDownload"] = filename |
||
861 | item["replace"]["FileDownload"] = anchor |
||
862 | |||
863 | item["DocumentVersion"] = obj.getDocumentVersion() |
||
864 | item["DocumentLocation"] = obj.getDocumentLocation() |
||
865 | item["DocumentType"] = obj.getDocumentType() |
||
866 | |||
867 | return item |
||
868 | |||
869 | |||
870 | class ajaxGetInstrumentMethods(BrowserView): |
||
871 | """ Returns the method assigned to the defined instrument. |
||
872 | uid: unique identifier of the instrument |
||
873 | """ |
||
874 | # Modified to return multiple methods after enabling multiple method |
||
875 | # for intruments. |
||
876 | def __call__(self): |
||
877 | out = { |
||
878 | "title": None, |
||
879 | "instrument": None, |
||
880 | "methods": [], |
||
881 | } |
||
882 | try: |
||
883 | plone.protect.CheckAuthenticator(self.request) |
||
884 | except Forbidden: |
||
885 | return json.dumps(out) |
||
886 | bsc = getToolByName(self, "senaite_catalog_setup") |
||
887 | results = bsc(portal_type="Instrument", |
||
888 | UID=self.request.get("uid", "0")) |
||
889 | instrument = results[0] if results and len(results) == 1 else None |
||
890 | if instrument: |
||
891 | instrument_obj = instrument.getObject() |
||
892 | out["title"] = instrument_obj.Title() |
||
893 | out["instrument"] = instrument.UID |
||
894 | # Handle multiple Methods per instrument |
||
895 | methods = instrument_obj.getMethods() |
||
896 | for method in methods: |
||
897 | out["methods"].append({ |
||
898 | "uid": method.UID(), |
||
899 | "title": method.Title(), |
||
900 | }) |
||
901 | return json.dumps(out) |
||
902 |