| 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 |